コンストラクタ

コンストラクタ (constructor) は新しいオブジェクト ──具体的には複合型のインスタンス── を構築する関数のことです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 のオブジェクト構築システムはこういったケースに対応でき、その他にも機能を持ちます。

コンストラクタメソッド

Julia のコンストラクタは他の関数と同様であり、その振る舞いはメソッドの組み合わせとして定義されます。つまりコンストラクタに機能を追加したいなら、新しくメソッドを定義すれば済みます。例えば、上の Foo オブジェクトに一つの引数を受け取って barbaz にその値を設定するコンストラクタメソッドを追加するには、次のようにします:

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

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

barbaz フィールドにデフォルト値を提供するゼロ引数の Foo コンストラクタをさらに追加してみましょう:

julia> Foo() = Foo(0)
Foo

julia> Foo()
Foo(0, 0)

ここではゼロ引数のコンストラクタメソッドが一引数のコンストラクタメソッドを呼び、それがさらに (自動的に定義された) 二引数のコンストラクタメソッドを呼んでいます。すぐに説明される理由により、通常のメソッドのように定義したこういったコンストラクタメソッドを外部コンストラクタメソッド (outer constructor method) と呼びます。外部コンストラクタメソッドがオブジェクトの構築に使えるのは他のコンストラクタメソッド (デフォルトのものなど) だけです。

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

外部コンストラクタメソッドはオブジェクトの構築をより簡単にするという問題を解決しますが、この章の最初で示した二つの問題に対処することはできません: 不変条件の強制と、自己参照的なオブジェクトの構築です。この二つの問題の解決には内部コンストラクタメソッド (inner constructor method) を使うことになります。内部コンストラクタメソッドは基本的に外部コンストラクタメソッドと同様ですが、二つの違いがあります:

  1. 通常のメソッドとは違い、型宣言と同じブロックで定義されます。
  2. ローカルに定義される特殊な関数 new へのアクセスを持ちます。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

ただしこの型が mutable として宣言されていれば、フィールドの値を直接変更してこの不変条件を破ることができます。もちろんオブジェクトの内部情報をいじるのは招かれざる操作であり、行うべきではありません。

外部コンストラクタメソッドは誰でも後から追加できますが、内部コンストラクタメソッドを後から追加する方法はありません。外部コンストラクタメソッドは他のコンストラクタメソッドを呼ぶことでしかオブジェクトを構築できないので、オブジェクトの構築では必ずどこかで内部コンストラクタメソッドが呼ばれます。これにより、被宣言型の任意のオブジェクトが型の定義と共に提供される内部コンストラクタメソッドで構築されることが保証され、型の不変条件をある程度強制できるようになります。

内部コンストラクタメソッドが一つでも定義されると、デフォルトのコンストラクタメソッドは提供されません。必要な内部コンストラクタはあなたが実装するものと想定されます。デフォルトのコンストラクタはオブジェクトの全てのフィールドを (フィールドと同様に型の付いた) パラメータとして受け取り、それを new に適用し、その返り値のオブジェクトを返す関数と等価です:

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

この宣言は最初に示した明示的な内部コンストラクタメソッドを持たない定義と同様の効果を持ちます。また、次の二つの型は等価です ──T1 はデフォルトのコンストラクタを持ち、T2 は明示的なコンストラクタを持ちます:

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

なんてことはない型に思えるかもしれませんが、この型のインスタンスの構築方法を考えると問題が明らかになります。もし SelfReferential のインスタンス a が存在するなら、二つ目のインスタンスは次の呼び出しで作成できます:

julia> b = SelfReferential(a)

では最初のインスタンスはどう作成するのでしょうか? obj フィールドに対する正当な値として使えるインスタンスは存在しません。唯一の解決策は obj フィールドに値が代入されていない不完全に初期化された SelfReferential のインスタンスの構築を許し、その不完全なインスタンスを使って他のインスタンス (例えばそれ自身) の obj フィールドを埋めるというものです。

不完全に初期化されたオブジェクトを構築できるように、Julia では型が持つフィールドより少ない個数の引数で new を呼び出せるようになっています。こうして 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
           data
           Incomplete() = new()
       end

julia> z = Incomplete();

未初期化のフィールドを作成することはできますが、未初期化の参照にアクセスすればすぐにエラーが発生します:

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

このため null を何度も確認する必要はありません。ただし参照でないオブジェクトフィールドも存在します。Julia では他のオブジェクトを参照しない自己充足的な型をプレーンデータ (plain data) と呼びます。プレーンデータはプリミティブ型 (Int など) または他のプレーンデータな不変構造体から構成され、そのフィールドの初期値は未定義です:

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

julia> HasPlain()
HasPlain(438103441441)

プレーンデータの配列も同様の振る舞いをします。

内部コンストラクタメソッドでは不完全な値を他の関数に渡して、インスタンスの完全な構築をその関数に任せることができます:

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

コンストラクタが返す不完全なオブジェクトと同様に、もし complete_mecomplete_me が呼び出す関数が新しい Lazy オブジェクトの data フィールドに初期化しないでアクセスすると、すぐにエラーが発生します。

パラメトリックコンストラクタ

パラメトリック型はコンストラクタに関する新しい話題を提供します。パラメトリック型の節で説明したように、デフォルトではパラメトリック複合型のインスタンスは型パラメータを明示的に示して構築されるか、コンストラクタの引数から推論された型パラメータで構築されるかのいずれかです。例を示します:

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

julia> Point(1,2) ## T は推論される
Point{Int64}(1, 2)

julia> Point(1.0,2.5) ## T は推論される
Point{Float64}(1.0, 2.5)

julia> Point(1,2.5) ## T は推論される
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
  Point(::T, ::T) where T<:Real at none:2

julia> Point{Int64}(1, 2) ## T は明示される
Point{Int64}(1, 2)

julia> Point{Int64}(1.0,2.5) ## T は明示される
ERROR: InexactError: Int64(2.5)
Stacktrace:
[...]

julia> Point{Float64}(1.0, 2.5) ## T は明示される
Point{Float64}(1.0, 2.5)

julia> Point{Float64}(1,2) ## T は明示される
Point{Float64}(1.0, 2.0)

ここから分かるように、型パラメータを指定してコンストラクタを呼び出したときの引数はそのパラメータから定まるフィールドの型に変換されます。例えば Point{Int64}(1,2) は正しく動作しますが、Point{Int64}(1.0,2.5) では 2.5Int64 に変換するときに InexactError が発生します。型パラメータがコンストラクタへの引数から推論される場合には、その型に矛盾があってはいけません ──T が定まらなければエラーが発生します。今の例であれば、汎用的な Point コンストラクタに渡される二つの引数は同じ実数型である必要があります。

実は Point, Point{Float64}, Point{Int64} は全て異なるコンストラクタ関数であり、同様に Point{T} は異なる T に対してそれぞれ異なるコンストラクタ関数を表します。内部コンストラクタメソッドを明示的に定義しないと、複合型 Point{T<:Real} の宣言は内部コンストラクタメソッド Point{T} を任意の型 T<:Real に対して自動的に定義し、それらは全てパラメトリックでないデフォルトの内部コンストラクタメソッドのように振る舞います。加えて単一の汎用内部コンストラクタメソッド Point も定義され、これは一致する実数値型の二つの引数を受け取ります。自動的に用意されるコンストラクタは次の宣言と等価です:

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);

二つの定義がそれぞれ呼び出すときの形をしていることに注目してください。struct ブロックに含まれる Point{T}(x,y) の定義は Point{Int64}(1,2) といった呼び出しで起動され、これに対して外部で宣言されるコンストラクタメソッドは同じ数値型の値の組が渡されたときの汎用的な処理を表します。外部の宣言に対しては型パラメータを明示しない 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 関数を使って 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, !Matched::T) where T<:Real at none:1

こういった呼び出しを賢く動作させる一般的な方法については型の変換と昇格の章を参照してください。少しネタバレをすると、次の外部コンストラクタメソッドを定義すると汎用的な 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 がデフォルトで提供する型パラメータ付きコンストラクタメソッドはかなり厳密であるものの、その動作を賢く柔軟にするのは非常に簡単だということです。さらにコンストラクタは型システム・メソッド・多重ディスパッチを利用できるので、洗練された動作も多くの場合ですぐに定義できます。

ケーススタディ: 有理数

以上の事項をより深く理解するには、パラメトリック複合型とそのコンストラクタメソッドの現実的な例を見るのが一番でしょう。そこで、独自の有理数型 OutRational を実装する方法をこれから示します。この型は rational.jl で定義される組み込みの Rational 型に似せてあります:

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} <: RealOurRational が整数型の型パラメータを一つ取る実数値型であることを宣言しています。その次のフィールド宣言 num::Tden::TOurRational{T} オブジェクトが保持するのは T 型の整数二つであり、それぞれ有理数の分母と分子であることを示します。

興味深いのはここからです。OurRational は内部コンストラクタメソッドを一つ持ちます。このメソッドは num == den == 0 でないことを確認し、有理数オブジェクトを非負の分母を持つ通分された状態で作成します。このとき gcd 関数で計算した最大公約数で numden を割るという処理が行われ、gcd は第一引数 (den) と同じ符号を持つ整数を返すことから、通分後の den が非負であることが保証されます。OurRational の内部コンストラクタメソッドはこれ一つだけなので、OurRational オブジェクトが必ず通分された正規系で構築されることが確証できます。

OurRational は利便性のための外部コンストラクタメソッドをいくつか提供します。一つ目の "標準の" 汎用コンストラクタは引数として渡される分母と分子の型が一致するときに呼び出され、型パラメータ T は引数の型から推論されます。分母と分子の型が異なるときに呼び出される二つ目の汎用コンストラクタは共通の型への昇格を行い、その上で構築を型の引数が一致する外部コンストラクタメソッドに委譲します。三つ目の外部コンストラクタメソッドは分母を 1 とすることで整数を有理数に変換します。

外部コンストラクタメソッドの定義の後には 演算子の定義が続きます。これは MyRational を中置で (1 ⊘ 2 のように) 表記するための演算子であり、Julia の Rational における // 演算子と同様です。デフォルトでは は完全に未定義の演算子であり、この定義の前には意味を持ちません。しかしこの定義により、演算子の振る舞いは有理数の節で説明したものと同様となります ──たった数行で全ての振る舞いが定義できます。最初の定義は最も基礎的であり、整数 a, b に対する a ⊘ bOurRatoinal コンストラクタを使って OurRatoinal を構築することを表します。 のオペランドのいずれかが最初から有理数のときは、最終的な有理数の計算が少しだけ異なります: この計算が実際に行っているのは有理数と整数の除算です。最後に、 を複素整数に対して適用すると 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} のとき) に呼ばれる内部コンストラクタメソッドを持ちます。その上で型パラメータを自動的に決める (例えば PointPoint(1,2) のように呼び出せるようにするための) 外部コンストラクタメソッドも定義でき、そのとき外部コンストラクタメソッドは内部コンストラクタメソッドを呼び出してインスタンスを作成します。しかし、ときには内部コンストラクタメソッドを提供しない方が理にかなっている場合もあります。型パラメータを手動で設定できないようにするためです。

例えば、ベクトルとその和をまとめて保持する型の定義を考えます:

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 よりも大きな型にしたい場合があることです。例えば TInt32 でも、SInt64 とするのが望ましいかもしれません。そのときは SummedArray{Int32,Int32} 型のインスタンスを構築できるインターフェースを避けるべきです。これを実現する一つの方法は SummedArray に対するコンストラクタメソッドを struct の定義ブロックでのみ提供し、デフォルトコンストラクタメソッドを生成しないようにするというものです:

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:4

このコンストラクタは SummedArray(a) のように呼び出します。new{T,S} という構文は構築される型の型パラメータを指定しており、この new{T,S}SummedArray{T,S} を意味します。new{T,S} はどんな内部コンストラクタメソッドの定義でも利用できますが、利便性のため new{} への型パラメータは (可能なら) 構築される型と一致する値として自動的に導出されるようになっています。


  1. 命名について: 本来「コンストラクタ」はある型のオブジェクトを構築する関数全体を指しますが、その中の特定のメソッドだけを指して「コンストラクタ」という言葉を使うこともあります。メソッドとしてのコンストラクタを指しているのか関数としてのコンストラクタを指しているのかは、文脈を見れば通常は分かります。例えば特定のメソッドと他のメソッドとの違いを議論するというよくある状況では、「コンストラクタ」がメソッドとしてのコンストラクタを意味しているのは明らかです。[return]

広告