Spec-Zone.ru › Julia 0.5

Конструкторы

Конструкторы [1] — это функции, создающие новые объекты — в частности, экземпляры Составных типов. В Julia объекты типов также служат в качестве функций-конструкторов: они создают новые экземпляры самих себя, когда применяются к кортежу аргументов как функция. Это уже кратко упоминалось при введении составных типов. Например:

type Foo
  bar
  baz
end

julia> foo = Foo(1,2)
Foo(1,2)

julia> foo.bar
1

julia> foo.baz
2

Для многих типов формирование новых объектов путём связывания значений их полей — всё, что нужно для создания экземпляров. Однако существуют случаи, когда при создании составных объектов требуется больше функциональности. Иногда необходимо соблюдать инварианты, либо проверяя аргументы, либо преобразуя их. Рекурсивные структуры данных, особенно те, которые могут быть самоссылочными, часто нельзя чисто создать, не создав сначала неполное состояние, а затем программно изменить его, чтобы оно стало целым, как отдельный шаг от создания объекта. Иногда просто удобно создавать объекты с меньшим или другим типом параметров, чем у них есть поля. Система Julia для создания объектов решает все эти и другие случаи.

[1] Номенклатура: хотя термин «конструктор» обычно относится к всей функции, которая создаёт объекты определённого типа, принято немного злоупотреблять терминологией и называть конкретные методы-конструкторы «конструкторами». В таких ситуациях из контекста обычно ясно, что термин используется для обозначения «метода-конструктора», а не «функции-конструктора», особенно если он часто используется для выделения конкретного метода конструктора среди всех остальных.

Внешние методы-конструкторы

Конструктор — это такая же функция в Julia, как и любая другая, и его общее поведение определяется совокупным поведением его методов. Соответственно, вы можете добавить функциональность к конструктору, просто определив новые методы. Например, предположим, что вы хотите добавить метод-конструктор для Foo объектов, принимающий только один аргумент и использующий данное значение для обоих bar и baz полей. Это просто:

Foo(x) = Foo(x,x)

julia> Foo(1)
Foo(1,1)

Вы также могли бы добавить метод-конструктор без аргументов Foo , который предоставляет значения по умолчанию для обоих bar и baz полей:

Foo() = Foo(0)

julia> Foo()
Foo(0,0)

Здесь метод-конструктор без аргументов вызывает метод-конструктор с одним аргументом, который, в свою очередь, вызывает автоматически предоставляемый метод-конструктор с двумя аргументами. По причинам, которые станут ясны очень скоро, дополнительные методы-конструкторы, объявленные как обычные методы, так и называются внешними методами-конструкторами. Внешние методы-конструкторы могут создавать новый экземпляр только вызывая другой метод-конструктор, такой как автоматически предоставляемые значения по умолчанию.

Внутренние методы-конструкторы

В то время как внешние методы-конструкторы успешно решают проблему предоставления дополнительных удобных методов для создания объектов, они не решают две другие проблемы, упомянутые в начале этой главы: обеспечение инвариантов и возможность создания самоссылочных объектов. Для этих проблем требуются внутренние методы-конструкторы. Внутренний метод-конструктор очень похож на внешний метод-конструктор, но с двумя отличиями:

  1. Он объявляется внутри блока объявления типа, а не вне его, как обычные методы.
  2. Он имеет доступ к специальной локальной функции, называемой new , которая создаёт объекты типа блока.

Например, предположим, что необходимо объявить тип, содержащий пару вещественных чисел, при условии, что первое число не больше второго. Можно объявить его так:

type OrderedPair
  x::Real
  y::Real

  OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
end

Теперь OrderedPair объекты можно создавать только так, чтобы x <= y:

julia> OrderedPair(1,2)
OrderedPair(1,2)

julia> OrderedPair(2,1)
ERROR: out of order
 in OrderedPair(::Int64, ::Int64) at ./none:5
 ...

Вы все равно можете непосредственно изменить значения полей, чтобы нарушить этот инвариант, но вмешательство во внутреннее устройство объекта без приглашения считается плохой формой. Вы (или кто-то другой) также можете в любой момент предоставить дополнительные внешние методы-конструкторы, но после объявления типа нельзя добавить больше внутренних методов-конструкторов. Поскольку внешние методы-конструкторы могут создавать объекты только вызывая другие методы-конструкторы, в конечном счёте, для создания объекта должен быть вызван какой-то внутренний конструктор. Это гарантирует, что все объекты объявленного типа должны появиться в результате вызова одного из предоставляемых внутренними методами-конструкторами, тем самым обеспечивая некоторую степень соблюдения инвариантов типа.

Конечно, если тип объявлен как immutable, то обеспечиваются все инварианты, предоставляемые конструктором. Это важная рекомендация при принятии решения о том, должен ли быть неизменяемым тип.

Если определён хотя бы один внутренний метод-конструктор, то метод-конструктор по умолчанию не предоставляется: предполагается, что вы сами предоставили все необходимые внутренние конструкторы. Конструктор по умолчанию эквивалентен написанию собственного внутреннего метода-конструктора, который принимает все поля объекта в качестве параметров (ограниченные соответствующим типом, если соответствующее поле имеет тип), и передает их new, возвращая полученный объект:

type Foo
  bar
  baz

  Foo(bar,baz) = new(bar,baz)
end

Это объявление имеет тот же эффект, что и предыдущее определение типа Foo без явного внутреннего метода-конструктора. Следующие два типа эквивалентны — один с конструктором по умолчанию, другой с явным конструктором:

type T1
  x::Int64
end

type T2
  x::Int64
  T2(x) = new(x)
end

julia> T1(1)
T1(1)

julia> T2(1)
T2(1)

julia> T1(1.0)
T1(1)

julia> T2(1.0)
T2(1)

Считается хорошей формой предоставлять как можно меньше внутренних методов-конструкторов: только те, которые явно принимают все аргументы и обеспечивают необходимую проверку ошибок и преобразование. Дополнительные методы-конструкторы для удобства, предоставляющие значения по умолчанию или вспомогательные преобразования, должны предоставляться как внешние конструкторы, которые вызывают внутренние конструкторы для выполнения основной работы. Это разделение обычно довольно естественно.

Неполная инициализация

Окончательная проблема, которая до сих пор не решена, — это создание самоссылочных объектов или, более общо, рекурсивных структур данных. Поскольку основная сложность может быть не очевидна сразу, давайте кратко объясним её. Рассмотрим следующее рекурсивное объявление типа:

type SelfReferential
  obj::SelfReferential
end

Этот тип может показаться достаточно безобидным, пока не подумаешь о том, как создать его экземпляр. Если a — экземпляр SelfReferential, то второй экземпляр можно создать вызовом:

b = SelfReferential(a)

Но как создать первый экземпляр, когда ни один экземпляр не существует, чтобы предоставить значение для поля obj? Единственное решение — разрешить создание неполностью инициализированного экземпляра SelfReferential с неопределённым полем obj, и использовать этот неполный экземпляр как допустимое значение для поля obj другого экземпляра, например, самого себя.

Чтобы разрешить создание неполностью инициализированных объектов, Julia позволяет вызывать функцию new с количеством аргументов меньше, чем количество полей типа, возвращая объект с неопределёнными полями, которые не инициализированы. Затем внутренний метод-конструктор может использовать неполный объект, завершая его инициализацию перед возвратом. Здесь, например, мы ещё раз пытаемся определить тип SelfReferential с внутренним конструктором без аргументов, возвращающим экземпляры с полями obj, указывающими на себя:

type SelfReferential
  obj::SelfReferential

  SelfReferential() = (x = new(); x.obj = x)
end

Можно проверить, что этот конструктор работает и создаёт объекты, которые на самом деле являются самоссылочными:

julia> x = SelfReferential();

julia> is(x, x)
true

julia> is(x, x.obj)
true

julia> is(x, x.obj.obj)
true

Хотя обычно рекомендуется возвращать полностью инициализированный объект из внутреннего конструктора, можно возвращать и неполностью инициализированные объекты:

julia> type Incomplete
         xx
         Incomplete() = new()
       end

julia> z = Incomplete();

Хотя вы можете создавать объекты с неинициализированными полями, любой доступ к неинициализированной ссылке является немедленной ошибкой:

julia> z.xx
ERROR: UndefRefError: access to undefined reference
 ...

Это позволяет избежать необходимости постоянной проверки на значения null. Однако не все поля объектов являются ссылками. Julia рассматривает некоторые типы как «простые данные», что означает, что все их данные самодостаточны и не ссылаются на другие объекты. Простые типы данных включают битовые типы (например, Int) и неизменяемые структуры других простых типов данных. Начальное содержимое простого типа данных неопределено:

julia> type HasPlain
         n::Int
         HasPlain() = new()
       end

julia> HasPlain()
HasPlain(438103441441)

Массивы простых типов данных ведут себя аналогично.

Вы можете передавать неполные объекты другим функциям из внутренних конструкторов для делегирования их завершения:

type Lazy
  xx

  Lazy(v) = complete_me(new(), v)
end

Как и в случае с неполными объектами, возвращаемыми конструкторами, если complete_me или любой из его вызывающих функций пытаются получить доступ к полю xx объекта Lazy до его инициализации, произойдёт немедленная ошибка.

Параметрические конструкторы

Параметрические типы добавляют несколько нюансов к истории конструкторов. Вспомните из Параметрических типов, что по умолчанию экземпляры параметрических составных типов могут быть созданы либо с явно заданными параметрами типа, либо с параметрами типа, подразумеваемыми типами аргументов, переданных конструктору. Вот несколько примеров:

julia> type Point{T<:Real}
         x::T
         y::T
       end

## implicit T ##

julia> Point(1,2)
Point{Int64}(1,2)

julia> Point(1.0,2.5)
Point{Float64}(1.0,2.5)

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point{T<:Real}(::Int64, ::Float64)
Closest candidates are:
  Point{T<:Real}{T<:Real}(::T<:Real, !Matched::T<:Real) at none:2
  Point{T<:Real}{T}(::Any) at sysimg.jl:53
 ...

## explicit T ##

julia> Point{Int64}(1,2)
Point{Int64}(1,2)

julia> Point{Int64}(1.0,2.5)
ERROR: InexactError()
 in Point{Int64}(::Float64, ::Float64) at ./none:2
 ...

julia> Point{Float64}(1.0,2.5)
Point{Float64}(1.0,2.5)

julia> Point{Float64}(1,2)
Point{Float64}(1.0,2.0)

Как видно, для вызовов конструкторов с явными параметрами типа аргументы преобразуются в подразумеваемые типы полей: Point{Int64}(1,2) работает, но Point{Int64}(1.0,2.5) вызывает InexactError при преобразовании 2.5 в Int64. Когда тип подразумевается аргументами вызова конструктора, как в Point(1,2), типы аргументов должны совпадать — в противном случае тип T нельзя определить — но любая пара вещественных аргументов с совпадающим типом может быть передана универсальному конструктору Point.

На самом деле, Point, Point{Float64} и Point{Int64} — это разные функции-конструкторы. Фактически, Point{T} — это отдельная функция-конструктор для каждого типа T. Без явно заданных внутренних конструкторов объявление составного типа Point{T<:Real} автоматически предоставляет внутренний конструктор Point{T} для каждого возможного типа T<:Real, который ведет себя так же, как и непараметрические внутренние конструкторы по умолчанию. Он также предоставляет один общий внешний конструктор Point , который принимает пары вещественных аргументов, которые должны быть одного типа. Это автоматическое предоставление конструкторов эквивалентно следующему явному объявлению:

type Point{T<:Real}
  x::T
  y::T

  Point(x,y) = new(x,y)
end

Point{T<:Real}(x::T, y::T) = Point{T}(x,y)

Некоторые особенности определения параметрических конструкторов, используемые здесь, заслуживают комментария. Во-первых, объявления внутренних конструкторов всегда определяют методы Point{T} , а не методы общего Point конструктора функции. Поскольку Point не является конкретным типом, для него не имеет смысла иметь внутренние методы конструктора. Таким образом, объявление внутреннего метода Point(x,y) = new(x,y) предоставляет внутренний метод конструктора для каждого значения T. Именно это объявление метода определяет поведение вызовов конструктора с явными параметрами типа, такими как Point{Int64}(1,2) и Point{Float64}(1.0,2.0). С другой стороны, объявление внешнего конструктора определяет метод для общего Point конструктора, который применяется только к парам значений одного и того же вещественного типа. Это объявление позволяет работать вызовам конструктора без явных параметров типа, таким как Point(1,2) и Point(1.0,2.5). Поскольку объявление метода ограничивает аргументы одинаковым типом, вызовы, такие как Point(1,2.5), с аргументами разных типов, приводят к ошибкам "нет метода".

Предположим, что мы хотим заставить вызов конструктора Point(1,2.5) работать, «преобразуя» целое значение 1 в число с плавающей точкой 1.0. Самый простой способ добиться этого — определить дополнительный внешний метод конструктора:

julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);

Этот метод использует функцию convert(), чтобы явно преобразовать x в Float64, а затем делегирует построение общему конструктору для случая, когда оба аргумента являются Float64. С этим определением метода то, что ранее было MethodError, теперь успешно создает точку типа Point{Float64}:

julia> Point(1,2.5)
Point{Float64}(1.0,2.5)

julia> typeof(ans)
Point{Float64}

Однако другие аналогичные вызовы по-прежнему не работают:

julia> Point(1.5,2)
ERROR: MethodError: no method matching Point{T<:Real}(::Float64, ::Int64)
Closest candidates are:
  Point{T<:Real}{T<:Real}(::T<:Real, !Matched::T<:Real) at none:2
  Point{T<:Real}{T}(::Any) at sysimg.jl:53
 ...

Для гораздо более общего способа обработки всех таких вызовов разумным образом см. Преобразование и продвижение. Рискуя испортить интригу, мы можем раскрыть здесь, что для того, чтобы все вызовы к общему конструктору Point работали так, как ожидается, достаточно следующего определения внешнего метода:

julia> Point(x::Real, y::Real) = Point(promote(x,y)...);

Функция promote преобразует все свои аргументы в общий тип — в данном случае Float64. С этим определением метода конструктор Point повышает свои аргументы так же, как это делают числовые операторы, такие как +, и работает со всеми типами вещественных чисел:

julia> Point(1.5,2)
Point{Float64}(1.5,2.0)

julia> Point(1,1//2)
Point{Rational{Int64}}(1//1,1//2)

julia> Point(1.0,1//2)
Point{Float64}(1.0,0.5)

Таким образом, хотя неявно заданные конструкторы параметров, предоставляемые по умолчанию в Julia, достаточно строги, возможно, сделать их поведение более гибким, но разумным довольно легко. Более того, поскольку конструкторы могут использовать всю мощь системы типов, методы и множественный диспетчер, определение сложного поведения обычно довольно просто.

Случайное исследование: Рациональные числа

Возможно, наилучшим способом объединения всех этих фрагментов является представление реального примера параметрического составного типа и его методов конструктора. В этих целях вот начало rational.jl, который реализует рациональные числа Julia:

immutable Rational{T<:Integer} <: Real
    num::T
    den::T

    function Rational(num::T, den::T)
        if num == 0 && den == 0
            error("invalid rational: 0//0")
        end
        g = gcd(den, num)
        num = div(num, g)
        den = div(den, g)
        new(num, den)
    end
end
Rational{T<:Integer}(n::T, d::T) = Rational{T}(n,d)
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
Rational(n::Integer) = Rational(n,one(n))

//(n::Integer, d::Integer) = Rational(n,d)
//(x::Rational, y::Integer) = x.num // (x.den*y)
//(x::Integer, y::Rational) = (x*y.den) // y.num
//(x::Complex, y::Real) = complex(real(x)//y, imag(x)//y)
//(x::Real, y::Complex) = x*y'//real(y*y')

function //(x::Complex, y::Complex)
    xy = x*y'
    yy = real(y*y')
    complex(real(xy)//yy, imag(xy)//yy)
end

Первая строка — immutable Rational{T<:Int} <: Real — объявляет, что Rational принимает один параметр типа целого числа и является вещественным типом. Объявления полей num::T и den::T указывают, что данные, хранящиеся в объекте Rational{T} , представляют собой пару целых чисел типа T, одна из которых представляет собой числитель рационального значения, а другая — знаменатель.

Теперь все усложняется. Rational имеет один внутренний метод конструктора, который проверяет, что оба num и den не равны нулю, и гарантирует, что каждый рациональный создается в «нормализованной форме» со знаменателем неотрицательным. Это достигается путем деления заданных значений числителя и знаменателя на их наибольший общий делитель, вычисленный с помощью функции gcd. Поскольку gcd возвращает наибольший общий делитель своих аргументов со знаком, соответствующим первому аргументу (den здесь), после этого деления новое значение den гарантированно будет неотрицательным. Поскольку это единственный внутренний конструктор для Rational, мы можем быть уверены, что объекты Rational всегда создаются в этой нормализованной форме.

Rational также предоставляет несколько внешних методов конструктора для удобства. Первый — это «стандартный» общий конструктор, который выводит параметр типа T из типа числителя и знаменателя, когда они имеют одинаковый тип. Второй применяется, когда заданные значения числителя и знаменателя имеют разные типы: он повышает их до общего типа и затем делегирует построение внешнему конструктору для аргументов одинакового типа. Третий внешний конструктор преобразует целые числа в рациональные числа, предоставив значение 1 в качестве знаменателя.

После определений внешних конструкторов у нас есть ряд методов для оператора //, который предоставляет синтаксис для записи рациональных чисел. Перед этими определениями // является совершенно неопределенным оператором с только синтаксисом и без смысла. После этого он ведет себя так, как описано в Рациональные числа — его поведение полностью определено в этих нескольких строках. Первое и самое основное определение просто заставляет a//b построить Rational путем применения конструктора Rational к a и b , когда они являются целыми числами. Когда один из операндов // уже является рациональным числом, мы строим новое рациональное для полученного отношения немного по-другому; это поведение фактически идентично делению рационального числа на целое число. Наконец, применение // к комплексным целочисленным значениям создает экземпляр Complex{Rational} — комплексное число, вещественная и мнимая части которого являются рациональными:

julia> (1 + 2im)//(1 - 2im)
-3//5 + 4//5*im

julia> typeof(ans)
Complex{Rational{Int64}}

julia> ans <: Complex{Rational}
false

Таким образом, хотя оператор // обычно возвращает экземпляр Rational, если любой из его аргументов является комплексным целым числом, он вернет экземпляр Complex{Rational} вместо этого. Заинтересованный читатель должен рассмотреть возможность изучения остальной части rational.jl: она короткая, самодостаточная и реализует целый базовый тип Julia.

Конструкторы и преобразование

Конструкторы T(args...) в Julia реализуются как другие вызываемые объекты: методы добавляются к их типам. Тип типа — Type, поэтому все методы конструктора хранятся в таблице методов для типа Type. Это означает, что вы можете объявлять более гибкие конструкторы, например, конструкторы для абстрактных типов, путем явного определения методов для соответствующих типов.

Однако в некоторых случаях вы можете рассмотреть добавление методов к Base.convert вместо определения конструктора, поскольку Julia обращается к вызову convert(), если соответствующий конструктор не найден. Например, если конструктор T(args...) = ... не существует, вызывается Base.convert(::Type{T}, args...) = ....

convert широко используется в Julia, когда один тип должен быть преобразован в другой (например, при присваивании, ccall, и т.д.), и, как правило, должен быть определен (или успешен) только в том случае, если преобразование является без потерь. Например, convert(Int, 3.0) порождает 3, но convert(Int, 3.2) генерирует исключение InexactError. Если вы хотите определить конструктор для безпотерь преобразования из одного типа в другой, вам следует определить метод convert вместо этого.

С другой стороны, если ваш конструктор не представляет собой безпотерь преобразования или вообще не представляет «преобразование», лучше оставить его в качестве конструктора, а не метода convert. Например, конструктор Array{Int}() создает Array нулевой размерности типа Int, но на самом деле не является «преобразованием» из Int в Array.

Конструкторы только внешнего уровня

Как мы видели, типичный параметризованный тип имеет внутренние конструкторы, которые вызываются при известности параметров типа; например, они применяются к Point{Int} , но не к Point. Дополнительно можно добавить внешние конструкторы, которые автоматически определяют параметры типа, например, конструирование Point{Int} из вызова Point(1,2). Внешние конструкторы вызывают внутренние конструкторы для выполнения основной работы по созданию экземпляра. Однако в некоторых случаях предпочтительно не предоставлять внутренние конструкторы, чтобы предотвратить ручное запросы конкретных параметров типа.

Например, предположим, что мы определили тип, который хранит вектор вместе с точным представлением его суммы:

type SummedArray{T<:Number,S<:Number}
    data::Vector{T}
    sum::S
end

Проблема в том, что мы хотим, чтобы S был типом большего размера, чем T, чтобы мы могли суммировать многие элементы с меньшей потерей информации. Например, когда T равно Int32, мы хотим, чтобы S было Int64. Поэтому мы хотим избежать интерфейса, который позволяет пользователю создавать экземпляры типа SummedArray{Int32,Int32}. Один из способов сделать это — предоставить только внешний конструктор для SummedArray . Это можно сделать, используя определение метода по типу:

type SummedArray{T<:Number,S<:Number}
    data::Vector{T}
    sum::S

    function (::Type{SummedArray}){T}(a::Vector{T})
        S = widen(T)
        new{T,S}(a, sum(S, a))
    end
end

Этот конструктор будет вызван синтаксисом SummedArray(a). Синтаксис new{T,S} позволяет указать параметры для типа, который будет создан, т.е. этот вызов вернёт SummedArray{T,S}.

© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/constructors/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API