コンストラクタ

`

コンストラクタ

コンストラクタ[1]は新しいオブジェクトを作成する関数で、特に複合型のインスタンスの場合に使用されます。 Juliaでは、型オブジェクトはコンストラクタ関数としても機能します。 これを引数のタプルに関数として適用すると、自身の新しいインスタンスが作成されます。 ここまでは、複合型を紹介したときにすでに簡単に触れました。例えば、

julia> struct Foo
           bar
           baz
       end

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

julia> foo.bar
1

julia> foo.baz
2

新しいインスタンスを作成するには、各フィールドに値を束縛するだけでよい、という型はたくさんあります。 しかし、複合型のオブジェクトを作成するときには更に多くの機能が必要になる場合があります。 時には、不変性が必須の時は、引数を検査したり不変なものに変換したりすることもあります。 再帰的なデータ構造、 特に自己参照可能な構造は、はじめは不完全な状態で作成してからプログラムによって変更して全体を作るということを、オブジェクト作成とは別の過程として行わないと、きれいに構築できないことがよくあります。 また時には、実際のフィールドと比べて、パラメータの数が少なかったり、型が異なっていたりするオブジェクトを構築できた方が便利な場合もあります。 Juliaのオブジェクトを構成するシステムは、これらのすべてのケースに対応しています。

[1]

  命名法:「コンストラクタ」という用語は、通常、ある型に対して、その型のオブジェクトを構成する関数全体を指しますが、用語を少し乱用し、特定のコンストラクタメソッドも「コンストラクタ」と呼ぶことがよくあります。このようにしても、コンストラクタ関数ではなく、コンストラクタメソッドであることが、特にほかのすべてのメソッドの中から特定のコンストラクタメソッドを選び出して使っている場合は、通常は文脈から分かります。

`

外部コンストラクタメソッド

コンストラクタは、Juliaの他の関数と同じように、全体の挙動はメソッドの挙動の組み合わせで定義されています。 したがって、コンストラクタに機能を追加するには、新しいメソッドを定義するだけで可能です。 たとえば、Fooオブジェクトのコンストラクタメソッドを追加したいとします。 このオブジェクトは、引数を1つだけ取り、その値をbarフィールドとbazフィールドの両方に使用する単純なものです。

julia> Foo(x) = Foo(x,x)
Foo

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

また、引数のないFooコンストラクタメソッドを追加して、barフィールドとbazフィールドの両方にデフォルト値に設定することもできます。

julia> Foo() = Foo(0)
Foo

julia> Foo()
Foo(0, 0)

ここでは、引数のないコンストラクタメソッドが、1引数のコンストラクタメソッドを呼び出し、続いて、自動的に生成される2引数のコンストラクタメソッドが呼び出されます。 まもなく明らかにする理由から、通常のメソッドのように宣言して追加する、このようなコンストラクタメソッドは、外部 コンストラクタメソッドと呼ばれます。 外部コンストラクタメソッドが、新しいインスタンスを作成できるのは、別の既存のコンストラクタメソッドを呼び出す場合に限られ、デフォルト値を自動的に生成されるメソッドなどが呼ばれます。

`

内部コンストラクタメソッド

外部コンストラクタメソッドは、オブジェクトを生成するのに便利なメソッドを追加することに成功していますが、 この章のはじめのほうで取り上げた他の2つの事例には対処できません。 不変性の強制と自己参照オブジェクトの構築です。 こういった課題には、内部コンストラクタメソッドが必要です。 内部コンストラクタメソッドは、外部コンストラクタメソッドによく似ていますが、2つの違いがあります。

  1. 型宣言のブロックの内部で宣言されていて、通常のメソッドのようにブロックの外部で宣言されていません。
  2. ブロックの型のオブジェクトを作成する、ローカルに存在する特殊な関数newへのアクセスできます。

たとえば、一番目の数が二番目の数よりも大きくないという制約つきの、実数の組を保持する型を宣言したいとします。 この場合、次のように宣言することができます:

julia> struct 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
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] OrderedPair(::Int64, ::Int64) at ./none:4
 [3] top-level scope

型を可変に宣言すれば、フィールド値に直接アクセスして、不変性を破る変更ができますが、オブジェクトの内部で想定外のいじり方をするのはよくない手法です。 あなた(か他の誰か)は後から外部コンストラクタメソッドを追加することはできますが、型を宣言した後で内部コンストラクタメソッドを追加することはできません。 外部コンストラクタメソッドがオブジェクトを作成するには、他のコンストラクタメソッドを呼び出すしかないため、最終的には、内部コンストラクタのいづれかが必ず呼び出されてオブジェクトが作成されます。 このため、型の宣言されたオブジェクトはすべて、その型に備わった内部コンストラクタメソッドの1つから生成されることが保証され、型の不変性がある程度強制されます。

内部コンストラクタメソッドが定義されている場合は、デフォルトのコンストラクタメソッドは生成されません。 必要とするすべての内部コンストラクタはすべて自作することを前提としています。 デフォルトのコンストラクタがするのと同等のことは、以下のような独自の内部コンストラクタメソッドを自作して実現できます。 オブジェクトのすべてのフィールドを引数とし、(対応するフィールドに型を指定している場合は正しい型を設定し)、その引数をnewに渡して、戻ってくるオブジェクト自身の戻り値とするような内部コンストラクタメソッドです。

julia> struct Foo
           bar
           baz
           Foo(bar,baz) = new(bar,baz)
       end

この宣言は、以前のFoo型の定義で、明示的に内部コンストラクタメソッドを宣言していないものと同じ効果を持ちます。 次の2つの型は同等です。一つはデフォルトのコンストラクタを使うもので、もう一つは明示的なコンストラクタを使うものです。

julia> struct T1
           x::Int64
       end

julia> struct 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)

できるだけ内部コンストラクタメソッドを少なくするのは良い方法だと考えられています。 すべての引数を明示的に取り、必須であるエラーチェックと変換を強制するメソッドだけにします。 デフォルト値や補助的な変換を提供する便利なコンストラクタメソッドを追加する時は、内部コンストラクタを呼び出す外部コンストラクタとして作成し、重い作業を行うようにすべきです。 このように分離するのは、通常まったく自然なことです。

`

不完全な初期化

まだ説明していない最後の問題は、自己参照オブジェクト、さらに一般的には再帰的なデータ構造の生成です。 根源的な難しさはすぐには分からないかもしれないので、簡単に説明しましょう。次の再帰型宣言を考えてみましょう。

julia> mutable struct SelfReferential
           obj::SelfReferential
       end

この型は、どうやってインスタンス化するかを考えなければ、問題ないように見えるかもしれません。 aというSelfReferentialのインスタンスがあれば、呼び出しによって2つ目のインスタンスを作成できます。

julia> b = SelfReferential(a)

しかし、objフィールドに使う有効な値のインスタンスがなければ、最初のインスタンスはどうやって作成するのでしょうか? 唯一の解決法は、objフィールドに、なにも代入されていない初期化の不完全なSelfReferentialのインスタンスを作成し、その不完全なインスタンスをobjフィールドの有効な値として別のインスタンス(例えば自分自身)に使うという方法です。

初期化が不完全でもオブジェクトを作成できるように、Juliaでは、引数が型のフィールド数より少なくてもnew関数を呼び出すことができます。 このnew関数は、未指定のフィールドは初期化しないままで、オブジェクトを返します。 そのため、内部コンストラクタメソッドは不完全なオブジェクトを利用可能で、オブジェクトを返す前に初期化を完了します。 ここで、SelfReferential型の別の例を挙げてみましょう。 この例では、引数のない内部コンストラクタが、objフィールドが自身を指すインスタンスを返します。

julia> mutable struct SelfReferential
           obj::SelfReferential
           SelfReferential() = (x = new(); x.obj = x)
       end

このコンストラクタが実際に動作して、自己参照型のオブジェクトを作成することは、検証可能です。

julia> x = SelfReferential();

julia> x === x
true

julia> x === x.obj
true

julia> x === x.obj.obj
true

内部コンストラクタが完全に初期化されたオブジェクトを返す方が通常は良い手法ですが、初期化の不完全なオブジェクトを返すこともできます。

julia> mutable struct Incomplete
           xx
           Incomplete() = new()
       end

julia> z = Incomplete();

初期化されていないフィールドを持つオブジェクトを作成することはできますが、初期化されていない参照にアクセスすると、即座にエラーが生じます。

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

このため、null値を継続的にチェックする必要がなくなります。 ただし、すべてのオブジェクトフィールドが参照であるとは限りません。 Juliaは、いくつかの型を「プレーンデータ」とみなします。 「プレーンデータ」とは、すべてのデータが自身に含まれ、他のオブジェクトを参照していないことを意味します。 プレーンデータ型には、プリミティブ型(Intなど)や、他のプレーンデータ型から成る不変な複合型があります。 プレーンなデータ型は、初期状態では内容が定義されていません。

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

julia> HasPlain()
HasPlain(438103441441)

プレーンデータ型の配列は同様な動作をします。

不完全なオブジェクトを内部コンストラクタから他の関数​​に渡して、完成を委譲することができます。

julia> mutable struct Lazy
           xx
           Lazy(v) = complete_me(new(), v)
       end

コンストラクタから返される不完全なオブジェクトと同じように、complete_meなど呼び出し先が初期化する前にLazyオブジェクトのxxフィールドにアクセスしようとすると、即座にエラーが投げられます。

`

パラメータコンストラクタ

パラメータ型の場合は、今までのコンストラクタの話に新たな面が加わります。 パラメータ型 のことを復習しましょう。 デフォルトでは、パラメータ複合型をインスタンス化する際の型の指定は、 明示的に型パラメータを使う方法と、引数から暗黙的に推論させる方法があります。 いくつか例を示します。

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

julia> Point(1,2) ## implicit T ##
Point{Int64}(1, 2)

julia> Point(1.0,2.5) ## implicit T ##
Point{Float64}(1.0, 2.5)

julia> Point(1,2.5) ## implicit T ##
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
  Point(::T<:Real, ::T<:Real) where T<:Real at none:2

julia> Point{Int64}(1, 2) ## explicit T ##
Point{Int64}(1, 2)

julia> Point{Int64}(1.0,2.5) ## explicit T ##
ERROR: InexactError: Int64(Int64, 2.5)
Stacktrace:
[...]

julia> Point{Float64}(1.0, 2.5) ## explicit T ##
Point{Float64}(1.0, 2.5)

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

御覧のように、明示的に型パラメータを指定してコンストラクタを呼び出すと、引数は暗黙のうちにフィールドの型に変換されます。 Point{Int64}(1,2)とすると動作しますが、Point{Int64}(1.0,2.5)とすると、 2.5Int64 に変換する際に、 InexactError が発生します。 Point(1,2)のように、コンストラクタ呼び出しの引数によって、型が暗黙的に指定されている場合、引数の型どうしは一致させる必要があります。でなければ、Tを決定できません。任意の実引数のペアは、型が一致していれば、汎化型のPointコンストラクタに渡すことができます。

ここで見てきたことは、PointPoint{Float64}Point{Int64}すべてが異なるコンストラクタ関数だということです。 実際、Point{T}は、型Tごとにそれぞれ異なるコンストラクタ関数があります。 明示的に内部コンストラクタを定義しない場合は、複合型の宣言Point{T<:Real}は 、T<:Realを満たすそれぞれの型に対して、自動的に内部コンストラクタPoint{T}を生成し、パラメータを使わないデフォルトの内部コンストラクタのように振る舞います。 こうしたコンストラクタの自動生成は、以下の明示的な宣言と同等です。

julia> struct Point{T<:Real}
           x::T
           y::T
           Point{T}(x,y) where {T<:Real} = new(x,y)
       end

julia> Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);

各コンストラクタの定義と呼び出しは、同じような形式であることに注意してください。 Point{Int64}(1,2)のように呼び出すと、ブロック内の定義Point{T}(x,y)が呼び出されます。 一方、外部コンストラクタの宣言では、実数の同じ型の組にのみ適用される汎化コンストラクタPointのメソッドが定義されています。 この宣言によって、Point(1,2)Point(1.0,2.5)のように明示的に型パラメータをつけないコンストラクタの呼び出しが可能になります。 このメソッドの宣言では、引数が同じ型のものに制限されるため、Point(1,2.5)のような異なる型の引数を持つ呼び出しは、 "no method"のエラーがおこします。

Point(1,2.5)のようにコンストラクタを呼び出す時、整数値1を浮動小数点値1.0に「昇格」させたいとします。 これを実現する最も簡単な方法は、以下のように外部コンストラクタメソッドの定義を追加することです。

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

このメソッドは、convert()関数を使用して、xFloat64 に変換し、両方の引数が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(::Float64, ::Int64)
Closest candidates are:
  Point(::T<:Real, !Matched::T<:Real) where T<:Real at none:1

このような呼び出しをうまく動作させる一般的な方法については、[変換と昇格](@ ref conversion-and-promotion)を参照してください。 今までの話が無駄になるかもしれませんが、白状してしまうと、汎化コンストラクタ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の組込みの有理数を実装している部分を(少し修正していますが)見てみましょう。

julia> struct OurRational{T<:Integer} <: Real
           num::T
           den::T
           function OurRational{T}(num::T, den::T) where T<:Integer
               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

julia> OurRational(n::T, d::T) where {T<:Integer} = OurRational{T}(n,d)
OurRational

julia> OurRational(n::Integer, d::Integer) = OurRational(promote(n,d)...)
OurRational

julia> OurRational(n::Integer) = OurRational(n,one(n))
OurRational

julia> ⊘(n::Integer, d::Integer) = OurRational(n,d)
⊘ (generic function with 1 method)

julia> ⊘(x::OurRational, y::Integer) = x.num ⊘ (x.den*y)
⊘ (generic function with 2 methods)

julia> ⊘(x::Integer, y::OurRational) = (x*y.den) ⊘ y.num
⊘ (generic function with 3 methods)

julia> ⊘(x::Complex, y::Real) = complex(real(x) ⊘ y, imag(x) ⊘ y)
⊘ (generic function with 4 methods)

julia> ⊘(x::Real, y::Complex) = (x*y') ⊘ real(y*y')
⊘ (generic function with 5 methods)

julia> function ⊘(x::Complex, y::Complex)
           xy = x*y'
           yy = real(y*y')
           complex(real(xy) ⊘ yy, imag(xy) ⊘ yy)
       end
⊘ (generic function with 6 methods)

最初の行 のstruct OurRational{T<:Integer} <: Real では、OurRationalという型は、整数型の型パラメータ1個をとり、自身は実数型であることを宣言しています。 フィールドの宣言であるnum::Tden::Tは、OurRational{T}オブジェクトには整数型Tの組が保持されていて、これが有理数の分子と分母の組を表していることを示しています。

面白くなってきました。 OurRationalには内部コンストラクタメソッドが1つあって、numdenの両方が0ではないことを検査し、すべての有理数が、分母が非負の「既約分数」で構成されることを保証します。 これは、与えられた分子と分母の値を、最大公約数で割ることで得られ、最大公約数はgcd関数を使って計算されます。 gcd関数は、引数の最大公約数を最初の引数(ここではden)に一致する符号で返すため、 割り算の後に得られる新しいdenの値は非負であることが保証されます。 これはOurRationalの唯一の内部コンストラクタであるため、OurRationalオブジェクトは常にこうした正規化された形式で構築されていると断言できます。

OurRationalには便宜のため、いくつかの外部コンストラクタメソッドも備わっています。 一つ目は、分子と分母の型が同じ場合、その型から型パラメータTを推論する「標準」汎化コンストラクタです。 二つ目は、与えられた分子と分母の値の型が異なる場合に適用されます。 それらを共通の型に昇格させ、その後、型の一致する引数に対して動作する外部コンストラクタに生成を委譲します。 三つ目の外部コンストラクタは、分母に値1を与えて整数値を有理数に変換します。

外部コンストラクタの定義に続いては、// 演算子のための多数のメソッドがあります。これは、有理数を書くための構文です。 こういった定義がなければ、// は、構文だけで完全に意味のない未定義の演算子です。 定義をすると、 有理数で説明したような動作をします。その動作は全て、このファイルの数行で定義されています。 第一の、そして最も基本的な定義では、abが整数の時に、OurRationalコンストラクタを適用してa//bを生成します。 //の被演算子の1つが既に有理数である場合、新しい有理数は比の結果として少し違う方法で生成されます。 この動作は実際には有理数と整数の除算と同一です。 最後に、 //を複素有理数に適用して、Complex{OurRational}のインスタンスを作成します。 これは実部と虚部が有理数である複素数をあらわします。

julia> z = (1 + 2im) ⊘ (1 - 2im);

julia> typeof(z)
Complex{OurRational{Int64}}

julia> typeof(z) <: Complex{OurRational}
false

したがって、//演算子は通常OurRationalのインスタンスを返しますが、引数のいずれかが複素数の場合、替わりにComplex{OurRational}のインスタンスを返します。 興味のある読者は、rational.jlの残りの部分を熟読するといいでしょう。 短くてファイル内で完結していますが、Juliaの基本的な型である有理数型全体が実装されています。

`

外部限定コンストラクタ

これまで見てきたように、一般的なパラメータ型には、型パラメータが既知のときに呼び出される内部コンストラクタがあります。 例えば、PointではなくPoint{Int}が適用されます。 必要に応じて、型パラメータを自動的に決定する外部コンストラクタを追加することができます。例えば、Point(1,2)からPoint{Int}を呼び出すことができます。 外部のコンストラクタは内部のコンストラクタを呼び出して、インスタンスを作成する中核的な作業を行います。 しかし、特定の型パラメータを手動で呼び出せないように、内部コンストラクタを利用可能にしたくない場合もあります。

たとえば、ベクトルを格納し、さらにその合計を正確に表現する型を定義するとします。

julia> struct SummedArray{T<:Number,S<:Number}
           data::Vector{T}
           sum::S
       end

julia> SummedArray(Int32[1; 2; 3], Int32(6))
SummedArray{Int32,Int32}(Int32[1, 2, 3], 6)

ここで問題は、STより大きな型にして、多くの要素の合計を求める際情の報損失を少なくしたいということです。 たとえば、TInt32SInt64とします。 そして、ユーザーがSummedArray{Int32,Int32}といった型のインスタンスを構築できるようなインターフェイスは避けたいと考えています。 これを行う方法の1つは、SummedArrayコンストラクタのみを利用可能とし、定義ブロックの中でデフォルトのコンストラクタの生成を抑止することです。

julia> struct SummedArray{T<:Number,S<:Number}
           data::Vector{T}
           sum::S
           function SummedArray(a::Vector{T}) where T
               S = widen(T)
               new{T,S}(a, sum(S, a))
           end
       end

julia> SummedArray(Int32[1; 2; 3], Int32(6))
ERROR: MethodError: no method matching SummedArray(::Array{Int32,1}, ::Int32)
Closest candidates are:
  SummedArray(::Array{T,1}) where T at none:5

このコンストラクタはSummedArray(a)という構文によって呼び出されます。 new{T,S}という構文で、構築する型のパラメータを指定できます。 つまり、この呼び出しはSummedArray{T,S}を返します。 new{T,S}を任意のコンストラクタ定義で利用することができますが、利便性のため、new{}に対するパラメータは、可能な場合は、生成される型から自動的に推定されます。