伝統的に、プログラミング言語の型システムは大きく異なる二つの派閥に分類されます: プログラムに含まれる全ての式に対する型が実行に先立って計算できなければならない静的型システム (static type system) と、実行時に実際の値がプログラムによって計算されるまで型については何も分からない動的型システム (dynamic type system) です。オブジェクト指向を使うとコンパイル時に値の型が正確には決まらないコードを書けるようになるので、静的型付け言語にいくらかの柔軟性がもたらされます。異なる型に対して操作を行うコードが書ける機能を多相性 (polymorphism) と呼びます。古典的な動的型付け言語のコードは全て多相的です: 実行時に型を明示的に確認するときとオブジェクトが演算をサポートしていないときを除けば、任意の値の型は一切制限を受けません。

Julia の型システムは動的ですが、ある値が特定の型を持つことを表明できるようにすることで静的型システムの利点の一部を取り入れてもいます。この機能は効率的なコードを生成する上で大きな助けとなります。しかしさらに重要なのは、関数の型に応じたメソッドのディスパッチと言語の深い統合が可能になることです。メソッドのディスパッチはメソッドの章で詳細に説明しますが、ディスパッチを支えているのはこの章で説明する型システムです。

Julia で値の型を省略すると、その値は任意の型になれます。そのため Julia では型を全く明示しなくても様々な便利な関数を書くことができます。コードを書いた後に型を細かく表現する必要が生じたら、その "型無し" コードに少しずつ明示的な型注釈を加えていくことが可能です。注釈の追加には三つの目的があります: Julia の強力な多重ディスパッチ機構の活用、人間にとっての読みやすさの向上、そしてプログラマーのエラーの検出です。

型システムの言葉を使って説明すると、Julia の型システムは動的 (dynamic) で、名前的 (nominative) で、パラメトリック (parametric) です。総称型はパラメータ化が可能であり、型の間の階層関係は型の構造に互換性があるかどうかで推論されるのではなく明示的に宣言されます。Julia の型システムで特に変わっているのが、ある具象型が別の具象型の部分型になれないことです: 全ての具象型は final であり、上位型になれるのは抽象型だけです。一見これは過度な制限に思えるかもしれませんが、利点は様々あるのに対して欠点はほとんどありません。私たちの実感としては、構造が継承できることより振る舞いが継承できることの方がずっと重要であり、伝統的なオブジェクト指向言語ではこの両者を同時に継承するために大きな困難が生じています。

Julia の型システムが持つ高水準な特徴の中で最初に言及しておくべきものを示します:

Julia の型システムは強力かつ高い表現力を持ち、そうでありながらも明快かつ直感的で控えめなように設計されています。多くの Julia プログラマーは型を明示的に使うコードを書く必要がないかもしれません。しかし一部の問題では、型宣言を使うとコードがより明快で、単純で、高速で、頑健になります。

型宣言

式や変数に型注釈を加えるには :: 演算子を使います。型注釈を使う主な目的は次の通りです:

  1. プログラムが思った通りに実行されることを確認するアサーションとして使う。
  2. 型に関する追加の情報をコンパイラに提供する。コンパイラはこの情報をもとに性能を向上させる場合がある。

値を計算する式に付いている :: 演算子の意味は「左辺は右辺のインスタンスである」であり、式の型を確認したいときにいつでも利用できます。右辺が具象型であれば、左辺の値はその型の実装である必要があります ──全ての具象型は final であり、ある具象型の実装がその部分型の実装とはならないことを思い出してください。右辺の型が抽象型であれば、左辺の値がその抽象型の部分 (具象) 型の実装であることが確認されます。型のアサーションが確認できないと例外が発生し、確認できれば左辺の型の値が返ります:

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64

julia> (1+2)::Int
3

この機能を使うと任意の式に対する型の確認をインプレースに行えます。

代入の左辺 (あるいは local 宣言) の変数に :: 演算子を付けると、意味が少し変わります。この場合、C といった静的言語の型宣言と同じように、必ずその型を持つ変数が宣言されます。こうして宣言される変数の値はそれぞれ指定された型に convert で変換されます:

julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> foo()
100

julia> typeof(ans)
Int8

値の代入で知らない間に変数の型が変わって性能が落ちる「うっかり」を回避するのにこの機能は役立ちます。

型宣言におけるこの振る舞いは次に示す特定の文脈でのみ起こります:

local x::Int8  # ローカル変数の選言
x::Int8 = 10   # 代入の左辺

さらにこの振る舞いは宣言より前の部分を含む現在のスコープ全体に影響を及ぼします。また現在のバージョンの Julia は定数型のグローバル変数をサポートしないので、型宣言はグローバルスコープ (例えば REPL) で利用できません。

型宣言は関数の定義にも付けることができます:

function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

こういった関数からの値の返却は型宣言付きの変数への代入と同じように行われます: つまり値は必ず Float64 に変換されます。

抽象型

抽象型はインスタンスを持たず、型グラフのノードとなるのが唯一の役割です。つまり様々な具象型の親となって型の間の関係を表すために抽象型は存在します。インスタンスを持たない抽象型を最初に説明するのは、抽象型が Julia の型システムの背骨であるためです。抽象型が理論的な型の階層を形成することで、型システムはオブジェクトの実装の寄せ集め以上のものとなります。

整数と浮動小数点数の章では数値を表す具象型を多く紹介しました。具体的には Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, Float64 です。表現とサイズは違いますが、Int8, Int16, Int32, Int64, Int128 には符号付き整数型を表すという共通点があります。同様に UInt8, UInt16, UInt32, UInt64, UInt128 はどれも符号無し整数型を表し、また Float16, Float32, Float64 は浮動小数点数型を表す点で他の数値型と異なっています。

あるコードが (例えば) 整数型の引数に対して意味を持ち、どの整数型であっても構わないという状況はよく発生します。例えば最大公約数アルゴリズムは全ての整数型に対して動作します (が、浮動小数点数型には適用できません)。抽象型 (abstract type) を使うと型の階層を構成でき、具象型に対する文脈を提供できます。抽象型を使うと例えば整数を表す型に対するプログラミングが簡単に可能になり、そのときアルゴリズムを具体的な整数型に制限する必要はありません。

抽象型は abstract type キーワードを使って宣言します。抽象型宣言の一般的な構文は次の通りです:

abstract type «name» end
abstract type «name» <: «supertype» end

キーワード abstract type«name» という名前の新しい抽象型を作成します。この名前に <: と既存の型を続けると、新しく作られる抽象型が指定した "親型" の部分型 (subtype) となります。

上位型が与えられないときのデフォルトの上位型は Any ──全ての型の上位型であり、任意のオブジェクトがインスタンスとなる組み込みの抽象型── です。Any は型グラフの頂点に位置するので、型理論では Any を「トップ」と呼びます。Julia には型グラフの底を意味する「ボトム」と呼ばれる組み込みの抽象型も存在します。ボトムは Union{} と表記し、トップの逆として振る舞います: Union{} のインスタンスとなるオブジェクトは存在せず、任意の型は Union{} の上位型です。

Julia の数値型の階層を構成する抽象型を見てみましょう:

abstract type Number end
abstract type Real     <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer  <: Real end
abstract type Signed   <: Integer end
abstract type Unsigned <: Integer end

NumberAny の直接の子であり、RealNumber の直接の子です。Real には二つの子 IntegerAbstractFloat があり、世界を整数の表現と実数の表現に分けています (Real は他にも子を持ちますが、これは後で触れます)。実数の表現にはもちろん浮動小数点数型が含まれますが、有理数型なども含まれます。そのため AbstractFloatReal の真の部分型となっており、AbstractFloat には浮動小数点数を使った実数の表現だけが含まれます。Integer はさらに SignedUnsigned に分かれます。

<: 演算子は「...の部分型である」を意味します。上記のように型宣言で使うと、右辺の型が新しく作成される左辺の型の直接の上位型であることが宣言されます。式の中で部分型演算子としても利用でき、そうすると左辺が右辺の部分型であるとき true が返ります:

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

抽象型の重要な使い方の一つが具象型に対するデフォルト実装の提供です。簡単な例として次のコードを考えます:

function myplus(x,y)
    x+y
end

まず、上記の引数宣言は myplus(x::Any,y::Any) と等しいことを指摘しておきます。この関数を myplus(2,5) と呼び出したとしましょう。このときディスパッチャが与えられた引数とマッチする myplus という名前のメソッドの中で最も特定的なものを選択します (多重ディスパッチについて詳しくはメソッドの章を参照してください)。

上の myplus(x, y) より特定的なメソッドがないと仮定すれば、Julia は続いて汎用的な myplus を二つの Int を受け取るメソッドに特殊化したものを定義・コンパイルします。つまり、Julia は次の関数の定義とコンパイルを自動的に行います:

function myplus(x::Int,y::Int)
    x+y
end

そして最後に、この特殊化されたメソッドを呼び出します。

このように、抽象型を使って汎用的な関数を書いておけば、後からそれを様々な具象型の組み合わせに対するデフォルトのメソッドとして使うことができます。多重ディスパッチのおかげで、デフォルトのメソッドとより特定的なメソッドのどちらを使うかはプログラマーの側から完全に制御できます。

ここで重要な点として、抽象型を引数とする関数を使ったとしても性能は低下はしないことを指摘しておきます。関数に渡された引数の型のタプルごとにコンパイルが毎回行われるためです。(ただし、引数が抽象型のコンテナである場合には性能が低下する可能性があります。詳しくはパフォーマンス Tips を見てください。)

プリミティブ型

注意

ほとんど場合で、独自のプリミティブ型を定義するよりも既存のプリミティブ型をラップして新しい複合型を作る方が好ましいことに注意してください。

プリミティブ型を定義する機能は LLVM がサポートする標準のプリミティブ型を Julia からブートストラップするためにあります。一度定義してしまえば、それ以外のプリミティブ型を定義する理由はほとんどありません。

プリミティブ型はプレーンオールドなビット列からなる具象型であり、整数と浮動小数点数がその古典的な例です。多くの言語では組み込みのプリミティブ型だけが利用できますが、Julia ではユーザーが独自のプリミティブ型を宣言できます。実は、標準のプリミティブ型も言語の中で定義されています:

primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end

primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end

primitive type Int8    <: Signed   8 end
primitive type UInt8   <: Unsigned 8 end
primitive type Int16   <: Signed   16 end
primitive type UInt16  <: Unsigned 16 end
primitive type Int32   <: Signed   32 end
primitive type UInt32  <: Unsigned 32 end
primitive type Int64   <: Signed   64 end
primitive type UInt64  <: Unsigned 64 end
primitive type Int128  <: Signed   128 end
primitive type UInt128 <: Unsigned 128 end

プリミティブ型を宣言するための一般的な構文は次の通りです:

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

ビット数 «bits» は型が必要とする格納領域の大きさを表し、名前 «name» は型の名前を表します。プリミティブ型は何らかの上位型の部分型としても宣言できます。上位型を書かないと新しい型は Any を直接の上位型に持つと自動的に解釈されます。例えば上に示した Bool 型の宣言は、真偽値の格納領域が 8 ビットであり、Integer を直接の上位型に持つことを表しています。現在 Julia がサポートしているのは 8 ビットの倍数のサイズを持つ型だけであり、上のコードで使われていないサイズを使うと高い確率で LLVM のバグを踏みます。そのため真偽値が本当に必要とするのは 1 ビットですが、8 ビットより小さい値をここで使うことはできません。

Bool, Int8, UInt8 という三つの型は全く同一の表現を持ちます: 八ビットのメモリ領域です。しかし Julia の型システムは名前的なので、同一の構造を持つこれらの型は交換可能ではありません。三つの型の根本的な違いはその上位型です: Bool の直接の上位型は Integer であり、Int8 では Signed で、UInt8 では Unsigned です。これ以外の Bool, Int8, UInt8 の違いは全て振る舞い ──これらの型のオブジェクトを引数に受け取った関数の動作── にあります。名前的な型システムが必要な理由はここにあります: もし構造が型を決めるとしたら、型が振る舞いを決めるので、BoolInt8UInt8 に異なる振る舞いをさせることが不可能になります。

複合型

複合型 (composite type) は言語によって「レコード」「構造型」「オブジェクト」などと異なる名前で呼ばれる概念です。複合型は名前が付いたフィールドの集合であり、そのインスタンスは一つの値として扱われます。多くの言語において複合型はユーザーが定義できる唯一の型であり、Julia でも複合型は圧倒的に最もよく使われるユーザー定義型です。

C++, Java, Python, Ruby といった主流なオブジェクト指向言語では複合型に名前の付いた関数が関連付き、型と関数をまとめて「オブジェクト」と呼びます。Ruby や Smalltalk といった純粋なオブジェクト指向言語では、複合型かどうかに関わらず全ての値がオブジェクトとなります。C++ や Java といった比較的純粋でないオブジェクト指向言語では整数や浮動小数点数といった値はオブジェクトではなく、ユーザー定義の複合型だけがメソッドを持つ真のオブジェクトとなります。

Julia では全ての値がオブジェクトですが、関数は処理対象のオブジェクトに結び付きません。こうする必要がある理由は、Julia が関数呼び出しで呼び出すべきメソッドを決めるときに多重ディスパッチを使うためです。つまり関数の最初の引数の型からメソッドが決まるのではなく、関数に渡された全ての引数の型を使ってメソッドが選択されます (メソッドとディスパッチについて詳しくはメソッドの章を参照してください)。そのため関数が最初の引数に "属する" ようにするのは適切ではありません。オブジェクトの中に名前と共にメソッドをまとめて詰め込むのではなく、複数のメソッドを束ねて関数オブジェクトとして整理するこの仕組みは、現在 Julia の言語設計が持つ非常に強力な特徴の一つとなっています。

複合型は struct キーワードで作成します。struct の後にフィールドの名前からなるブロックが続き、名前には :: 演算子を使った型注釈を付けられます:

julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

型注釈を持たないフィールドの型は Any となります。つまりそのフィールドは任意の型の値を保持できます。

Foo 型の新しいオブジェクトは Foo を関数のように呼び出すことで作成します。そのとき引数には各フィールドに対する値を渡します:

julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

型を関数のように適用するとコンストラクタとなります。複合型を定義するとデフォルトコンストラクタと呼ばれるコンストラクタが自動的に二つ定義されます。一つは任意の引数を受け取ってconvert でそれぞれをフィールドの型へと変換するもので、もう一つはフィールドの型と同じ引数を受け取るものです。生成されるコンストラクタがこの二つである理由は、新しい定義を加えたときにデフォルトのコンストラクタをうっかり上書きしないようにするためです。

bar フィールドには型の制限がないので、どんな値でも渡すことができます。これに対して、baz フィールドには Int に変換できる値を渡す必要があります:

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]

フィールド名のリストは fieldnames 関数で取得できます:

julia> fieldnames(Foo)
(:bar, :baz, :qux)

複合オブジェクトのフィールドの値にアクセスするには、伝統的な foo.bar という記法を使います:

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

struct で宣言された複合型のオブジェクトは不変 (immutable) であり、構築した後に改変することはできません。この仕様は一見すると奇妙に思えるかもしれませんが、いくつか利点があります:

不変オブジェクトのフィールドは可変オブジェクト (配列など) にできますが、そういったオブジェクトは可変のままとなります。最初から不変であるオブジェクトを持つフィールドを変えられないというだけです。

可変な複合オブジェクトが必要な場合には mutable struct キーワードで宣言できます。これは次の節で説明します。

フィールドを持たない不変複合型はシングルトンとなります。そういった型のインスタンスは一つしか存在しません:

julia> struct NoFields
       end

julia> NoFields() === NoFields()
true

構築された "二つの" NoFields のインスタンスが実は同一であることを === 関数が確認しています。シングルトン型については後でさらに説明します。

複合型がどのように作成されるかについてはさらに説明すべき事項がありますが、そのためにはパラメトリック型メソッドの理解が必要です。また複合型の作成は非常に重要な事項なので、一つの章を使って説明します。

可変複合型

複合型を struct ではなく mutable struct と宣言すると、インスタンスを改変できるようになります:

julia> mutable struct Bar
           baz
           qux::Float64
       end

julia> bar = Bar("Hello", 1.5);

julia> bar.qux = 2.0
2.0

julia> bar.baz = 1//2
1//2

改変をサポートするために、可変複合型のオブジェクトは通常ヒープにアロケートされ安定なメモリアドレスが割り当てられます。可変オブジェクトは時間の経過とともに値を変える小さなコンテナのようなものであり、信頼できる特定方法はアドレスを使ったものしかありません。これに対して、不変型のインスタンスはフィールドの値によって特定できます──フィールドの値がそのオブジェクトに関する情報の全てです。型を可変にすべきか迷ったときは、フィールドの値が同じ二つのインスタンスが同一とみなされるかどうか、そしてその二つのインスタンスが時間の経過とともに変更されるかどうかを考えてください。同じ値を持つインスタンスが同一とみなされるなら、その型はおそらく不変にするべきです。

Julia における不変性の重要な特徴をまとめます:

被宣言型

これまでの節で議論した三種類の型 (抽象型・プリミティブ型・複合型) は非常によく似ており、次の重要な特徴が共通しています:

こういった共通の特徴により、これら三つの型は内部で DataType という同じ型のインスタンスとして表現されます。これらの型の型を確認すると DataType であることが分かります:

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

DataType は抽象型の具象型の両方を表せます。具象型を表す DataType は指定されたサイズと格納レイアウト、そして (省略可能な) フィールド名を持ちます。例えばプリミティブ型はゼロでないサイズを持ったフィールド名を持たない DataType であり、複合型はフィールド名を持つこともあれば持たない (そしてサイズがゼロとなる) こともある DataType です。

Julia の型システムにおいて、全ての具象値は何らかの DataType のインスタンスです。

型共用体

型共用体 (type union) は Union キーワードで作られる特殊な抽象型であり、引数に渡される型の全てのインスタンスを含む型を表します:

julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}

julia> 1 :: IntOrString
1

julia> "Hello!" :: IntOrString
"Hello!"

julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64

多くの言語のコンパイラには型共用体を扱うために内部で使われる構文がありますが、Julia はそれをプログラマーに公開しています。Union 型があったとしても、型の数が少なければ1 Julia コンパイラは可能な型それぞれに対して分岐して特殊化を行うことで効率的なコードを生成できます。

Union 型の特に便利な使用例が Union{T, Nothing} です。T は任意の型で、Nothingnothing を唯一のインスタンスとするシングルトン型です。これは他の言語で Nullable, Option, Maybe などと呼ばれているパターンです。関数の引数や複合型のフィールドを Union{T, Nothing} と宣言すると、そこには T 型の値または nothing という値が存在しないことを表す特別なオブジェクトのいずれかを設定できます。詳しくは FAQ エントリーを見てください。

パラメトリック型

Julia の型システムが持つ重要で強力な特徴が、パラメトリックであることです。型はパラメータを取ることができ、これにより型の宣言が新しい型の族を作成します ──パラメータの値の組み合わせ一つにつき一つの新しい型です。

何らかの意味でジェネリックプログラミングをサポートする言語は多くあり、そういった言語ではデータ構造やアルゴリズムを型の指定なしに利用できます。例えば ML, Haskell, Ada, Eiffel, C++, Java, C#, F#, Scala がそのような言語の例です。一部の言語 (ML, Haskell, Scala) は真のパラメトリック多相をサポートしており、他の言語 (C++, Java) はアドホックなテンプレートベースのジェネリックプログラミングをサポートしています。ジェネリックプログラミングとパラメトリック型には非常にたくさんのバリエーションがあるので、ここでその全てを Julia のパラメトリック型と比べることはせず、Julia の型システムを説明することだけに集中します。ただし一つだけ、動的言語である Julia では全ての型をコンパイル時に決定する必要がないので、伝統的な静的パラメトリック型システムが直面してきた問題に比較的簡単に対応できることを指摘しておきます。

全ての被宣言型 (DataType のインスタンス) はパラメータ化でき、共通の特別な構文が用意されています。これからパラメトリック複合型・パラメトリック抽象型・パラメトリックプリミティブ型の順に説明します。

パラメトリック複合型

型パラメータは型の名前を波括弧で囲って表します:

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

この宣言は T 型の座標を二つ持つ新しいパラメトリック型 Point{T} を定義します。「T って何?」と思うかもしれませんが、これがまさにパラメトリック型のポイントです: T はどんな型にでもなれます (正確にはビット型の値にもなれますが、この T は型でないとコンパイルできません)。Point{Float64} は具象型であり、Point の定義に含まれる T の部分を Float64 に置き換えて定義される型と等価です。そのため、この一つの宣言は実際には Point{Float64}, Point{AbstractString}, Point{Int64} といった無数の型を宣言しています。これらは全て利用可能な具象型です:

julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

Point{Float64} 型は座標が 64 ビットの浮動小数点数型の点で、Point{AbstractString} 型は座標が文字列オブジェクト (参照: 文字列) の点です。

Point 自身も正当な型オブジェクトであり、Point{Float64}Point{AbstractString} といった全てのインスタンスを部分型として含みます:

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

他の型は、もちろん、Point の部分型ではありません:

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

また Point の具象型であって T の値が異なる型は、お互いに部分型にはなりません:

julia> Point{Float64} <: Point{Int64}
false

julia> Point{Float64} <: Point{Real}
false
注意

最後に述べた事実は非常に重要です: 例えば Float64 <: Real は成り立ちますが、だからといって Point{Float64} <: Point{Real} は成り立ちません。

型理論の用語を使って言い換えると、Julia の型パラメータは不変 (invariant) であり、共変 (convariant) あるいは反変 (contravariant) ではありません。これは実際的な理由によるものです: 理論上は Point{Float64} の任意のインスタンスを Point{Real} のインスタンスとしても問題はありませんが、この二つの型はメモリ上における表現が異なります:

Point{Float64} のオブジェクトを即値として格納することによる性能の向上は、配列でさらに大きくなります。Array{Float64} は 64 ビット浮動小数点数の値を連続的に並べたメモリ領域に格納できますが、Array{Real} は個別にアロケートした Real オブジェクトを指すポインタの配列でなければなりません。ポインタが指すのはボックス化された 64 ビット浮動小数点数値かもしれませんし、抽象型 Real の実装として宣言された任意に大きい複雑なオブジェクトかもしれません。

Point{Float64}Point{Real} の部分型ではないので、次のメソッドは Point{Float64} 型の引数に適用できません:

function norm(p::Point{Real})
    sqrt(p.x^2 + p.y^2)
end

任意の Real 型の部分型 T に対する Point{T} 型を受け取るメソッドを定義する正しい方法は次の通りです:

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

なお function norm(p::Point{T} where T<:Real) あるいは function norm(p::Point{T}) where T<:Real としても等価となります。詳しくは UnionAllの節を参照してください。

メソッドの章でさらに例を説明します。

Point オブジェクトはどのように構築するのでしょうか? コンストラクタの章で説明する方法を使えば独自のコンストラクタを定義できますが、独自のコンストラクタを宣言しない場合には新しい複合オブジェクトを作る方法は二つあります。一つは型パラメータを明示的に示す方法で、もう一つはオブジェクトコンストラクタへの引数から型を推論させる方法です。

Point{Float64} 型は PointTFloat64 に置き換えた具象型なので、コンストラクタもそのように利用できます:

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

julia> typeof(ans)
Point{Float64}

デフォルトコンストラクタでは各フィールドにちょうど一つの引数を与える必要があります:

julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
[...]

julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
[...]

パラメトリック型に対して生成されるデフォルトコンストラクタはオーバーライドできず、この一つだけです。このコンストラクタは任意の引数を受け取り、引数をフィールドの型に変換します。

多くの場合で、コンストラクタの引数の型を見れば作成されるオブジェクトの型が分かるので、作成される Point オブジェクトの厳密な型を示すのは冗長です。このため、型を示さない Point をコンストラクタとして使うこともできます。ただしこれが使えるのはパラメータ型 T が曖昧さなく決定されるときだけです:

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

julia> typeof(ans)
Point{Float64}

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

julia> typeof(ans)
Point{Int64}

今考えている Point の例では、T の型が曖昧さなく決定するのは Point への二つの引数が同じ型を持つときであり、かつそのときに限ります。この条件が成り立たないとコンストラクタは失敗して MethodError エラーが発生します:

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

こういった型が混ざる場合に適切に対処するコンストラクタメソッドを定義することもできますが、その議論はコンストラクタの章で行います。

パラメトリック抽象型

パラメトリック抽象型宣言は抽象型の集合を宣言します。構文はパラメトリック複合型に似ています:

julia> abstract type Pointy{T} end

この宣言により Pointy{T} という抽象型が整数値または型の T それぞれに対して定義されます。パラメトリック複合型と同様に、各インスタンスは Pointy の部分型となります:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

パラメトリック抽象型は不変であり、この点もパラメトリック複合型と同じです:

julia> Pointy{Float64} <: Pointy{Real}
false

julia> Pointy{Real} <: Pointy{Float64}
false

Pointy{<:Real}共変型を表す Julia の記法であり、Pointy{>:Int}反変型を表す Julia の記法です。正確に言うと、こういった記法は型の集合 (つまり UnionAll) を表します:

julia> Pointy{Float64} <: Pointy{<:Real}
true

julia> Pointy{Real} <: Pointy{>:Int}
true

通常の抽象型が具象型の間に便利な型階層を作成するのと同じように、パラメトリック抽象型はパラメトリック複合型の間に便利な型階層を作成します。例えば、Point{T}Pointy{T} の部分型であると宣言するには次のようにできます:

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

この宣言を行うと、任意の T に対して Point{T}Pointy{T} の部分型になります:

julia> Point{Float64} <: Pointy{Float64}
true

julia> Point{Real} <: Pointy{Real}
true

julia> Point{AbstractString} <: Pointy{AbstractString}
true

この関係も不変です:

julia> Point{Float64} <: Pointy{Real}
false

julia> Point{Float64} <: Pointy{<:Real}
true

Pointy のようなパラメトリック抽象型は何に使うのでしょうか? 例えば対角線 x = y 上にある点を実装するとしましょう。この型は座標が一つで済みます:

julia> struct DiagPoint{T} <: Pointy{T}
           x::T
       end

こうすると Point{Float64}DiagPoint{Float64} の両方が Pointy{Float64} 抽象型の実装となり、同様の関係が T にどんな型を選んでも成り立ちます。Pointy オブジェクトの共通インターフェースを PointDiagPoint に対して実装すれば、共通インターフェースに対するプログラミングが可能になります。しかしこれ以上のことを説明するには、次章のメソッドで説明されるメソッドとディスパッチに関する知識が必要です。

型パラメータが全ての型を取ってはいけない状況もあります。このような場合には T の範囲を次のように制限できます:

julia> abstract type Pointy{T<:Real} end

この宣言により T には Real の部分型だけが許され、Real の部分型でない T にはエラーが発生するようになります:

julia> Pointy{Float64}
Pointy{Float64}

julia> Pointy{Real}
Pointy{Real}

julia> Pointy{AbstractString}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}

julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got a value of type Int64

パラメトリック複合型に対する型パラメータも同様に制限できます:

struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

このパラメトリック型機構が役に立っている現実世界の例として、Julia が持つ不変型 Rational の実際の定義を次に示します。「二つの整数の正確な比」が表現されています:

struct Rational{T<:Integer} <: Real
    num::T
    den::T
end

比は整数値に対して意味を持つので、型パラメータ TInteger の部分型に制限されます。さらに整数の比は実数直線上の値を表すので、任意の Rational が抽象型 Real の部分型となるよう宣言されます。

タプル型

タプルは関数の引数の抽象化です ──関数の処理は含まれません。関数の引数の特徴的な性質は順序と型です。そのためタプル型はパラメータ化された不変型であり、一つのパラメータが一つのフィールドに対応します。例えば二要素のタプル型は次の不変型と基本的に同じです:

struct Tuple2{A,B}
    a::A
    b::B
end

ただし、この Tuple2 と実際のタプル型の間には三つの重要な違いがあります:

タプルの値は括弧とコンマを使って書きます。タプルが構築されるとき、適切なタプル型がその場で生成されます:

julia> typeof((1,"foo",2.5))
Tuple{Int64,String,Float64}

共変性が意味することに注意してください:

julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true

julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false

julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false

直感的に言うと、これは関数に渡された引数が関数のシグネチャに適合するとき、引数の型は関数のシグネチャの部分型になることに対応します。

可変長タプル型

タプル型の最後のパラメータには特殊な型 Vararg を指定できます。これは最後に並ぶ任意個の要素を表します:

julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString,Vararg{Int64,N} where N}

julia> isa(("1",), mytupletype)
true

julia> isa(("1",1), mytupletype)
true

julia> isa(("1",1,2), mytupletype)
true

julia> isa(("1",1,2,3.0), mytupletype)
false

Vararg{T} がゼロ個以上の T 型の要素に対応します。この可変長タプル型 (vararg tuple type) は可変長引数メソッドが受け取る引数を表現するのに使われます (参照: 可変長引数関数)。

Vararg{T,N} という型はちょうど N 要素の T 型に対応します。Tuple{Vararg{T,N}} の別名として NTuple{N,T} が提供されますが、これはちょうど N 要素の T 型からなるタプルを表します。

名前付きタプル型

名前付きタプル (named tuple) は NamedTuple 型のインスタンスです。この型は二つのパラメータを持ちます: フィールドの名前を与えるシンボルのタプルと、フィールドの型を与える型のタプルです。

julia> typeof((a=1,b="hello"))
NamedTuple{(:a, :b),Tuple{Int64,String}}

NamedTuple 型を struct 風の構文で簡単に宣言するための @NamedTuple マクロが提供されます。このマクロでは key::Type という宣言が可能であり、::Type を省略すると ::Any となります:

julia> @NamedTuple{a::Int, b::String}
NamedTuple{(:a, :b),Tuple{Int64,String}}

julia> @NamedTuple begin
           a::Int
           b::String
       end
NamedTuple{(:a, :b),Tuple{Int64,String}}

NamedTuple 型は単一のタプルを引数に受け取るコンストラクタとしても使用できます。コンストラクタに使う NamedTuple 型は両方のパラメータが指定された具象型でも、フィールドの名前だけが指定された抽象型でも構いません:

julia> @NamedTuple{a::Float32,b::String}((1,""))
(a = 1.0f0, b = "")

julia> NamedTuple{(:a, :b)}((1,""))
(a = 1, b = "")

フィールドの型を指定すると引数がその型に変換され、指定されなければ引数の型がそのまま使われます。

シングルトン型

ここで忘れずに言及しなければならない特殊な抽象パラメトリック型が一つあります: シングルトン型 (singleton type) です。シングルトン型とはインスタンスを一つだけしか持たない型のことを言います2

シングルトン型は Haskell, Scala, Ruby を含むいくつかの有名なプログラミングにも存在します。一般に "シングルトン型" と言えばインスタンスが単一の値である型を意味しますが、Julia のシングルトン型でも意味は同じです。

Type{T}

任意の型 T に対して、Type{T} はオブジェクト T を唯一のインスタンスに持つ抽象型を表します。この定義は分かりにくいと思うので、例を示します:

julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Float64})
false

julia> isa(Real, Type{Real})
true

julia> isa(Float64, Type{Real})
false

言い換えれば、isa(A,Type(B)) が真になるのは AB が同じ型オブジェクトのときであり、かつそのときに限ります。パラメータが付かない Type は全ての型オブジェクトをインスタンスに持つ抽象型であり、もちろん、Type{Float64} といった型も Type のインスタンスです:

julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

型でないオブジェクトは Type のインスタンスではありません:

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

パラメトリックメソッド型の変換を議論するまで、シングルトン型の使い道を説明するのは困難です。端的に言うと、シングルトン型があると関数の振る舞いを値としての型に対して特殊化できるようになります。これは振る舞いが引数の型ではなく明示的に渡される型に依存するメソッド (特にパラメトリックなメソッド) を書くときに役立ちます。

パラメトリックプリミティブ型

プリミティブ型もパラメトリックに宣言できます。例えば Julia のポインタ型をパラメトリックプリミティブ型として表現すれば、次の宣言となります:

# 32-bit システム:
primitive type Ptr{T} 32 end

# 64-bit システム:
primitive type Ptr{T} 64 end

通常のパラメトリック複合型と比べたときにこの宣言に関して少し奇妙なのが、型パラメータ T が型の定義で使われていないことです。T はただの抽象的なタグであり、新しい型の族に含まれる全ての型は型パラメータが違うだけで同一の構造として定義されます。例えば Ptr{Float64}Ptr{Int64} は異なる型となりますが、表現は同じです。そしてもちろん、全ての特殊化されたポインタ型は "傘型" (umbrella type) である Ptr の部分型となります:

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

UnionAll 型

これまでに Ptr のような型はそのインスタンス (Ptr{Int64} など) 全ての上位型であると説明しました。どういうことなのでしょうか? Ptr だけでは参照されるデータの型が分からずメモリ演算を行うことができないので、Ptr は通常の型ではないはずです。答えは「PtrUnionAll 型と呼ばれる異なる種類の型である」となります。Array のような他のパラメトリック型も同様に UnionAll 型であり、こういった型はパラメータが取り得る値それぞれに対する型を全て合併したものを表します。

通常 UnionAll 型はキーワード where を使って書かれます。例えば PtrPtr{T} where T と書いた方が正確であり、これは適当な T を使って Ptr{T} と書ける型からなる集合を表します。この文脈で T は型を値に取る変数のようなものなので、「型変数 (type variable)」と呼ばれます。一つの where が一つの型変数を導入し、複数のパラメータを持つ型には Array{T,N} where N where T のように where が複数付きます。

UnionAllA に対して A{B,C} として型を適用すると、まず A に含まれる一番外側の型変数が B に置換されます。この結果は別の UnionAll 型であり、その型の型変数がさらに C に置換されます。つまり A{B,C}A{B}{C} と等価です。Array{Float64} のように型を部分的にインスタンス化できるのはこのためです:Array{Float64} では一つ目のパラメータが固定されていますが、二つ目のパラメータは全ての可能な値を取ることができます。また明示的に where を使えば好きな場所のパラメータを固定でき、例えば任意の一次元配列を意味する型は Array{T,1} where T と書けます。

型変数は部分型関係を使って制限でき、例えば Array{T} where T<:IntegerInteger の部分型を要素とする配列を表します。また Array{<:Integer}Array{T} where T<:Integer の省略形として提供されます。型変数は上限と下限を持つことができ、例えば Array{T} where Int<:T<:NumberNumber の配列であって Int を格納できるものを表します (T が表す値の集合が最低でも Int を含む必要があるためです)。where T>:Int としても型変数 T の下限を指定でき、Array{>:Int}Array{T} where T>:Int と等価です。

where 式はネストするので、型変数の境界を指定するときに外の型変数を参照できます。例えば Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real が表すのは二要素のタプルであり、第一要素は何らかの Real で、第二要素は第一要素の型を要素とする任意の形状の Array です。

where キーワード自体もより複雑な宣言の中でネストできます。例えば、次の宣言で作られる二つの型を考えます:

julia> const T1 = Array{Array{T,1} where T, 1}
Array{Array{T,1} where T,1}

julia> const T2 = Array{Array{T,1}, 1} where T
Array{Array{T,1},1} where T

T1 型が定義するのは一次元配列の一次元配列であり、内部の配列は同じ型の要素を持ちますが、内部の配列同士を比べたときには配列が格納する要素の型は違っていて構いません。これに対して T2 型が定義するのは一次元配列の一次元配列であり、内部の配列は全て同じ型の配列である必要があります。T2 は抽象型であり例えば Array{Array{Int,1},1} <: T2 が成り立ちますが、T1 は具象型であることに注意してください。このため T1 はゼロ引数のコンストラクタ a=T1() で構築できますが、T2 はできません。

型変数を含む型を簡単に命名するための構文が提供されています。関数を定義する代入形式の構文と似たものです:

Vector{T} = Array{T,1}

これは const Vector = Array{T,1} where T と等価であり、例えば Vector{Float64} と書けば Array{Float64,1} と書いたのと同じことになります。"傘型" Vector はインスタンスとして第一引数 (配列の要素の型) が任意の型で第二引数 (配列の次元数) が 1 である任意の Array 型を持ちます。パラメトリック型に含まれる型変数を必ず完全に指定しなければならない言語ではこの機能があっても特に役に立ちませんが、Julia ではこの機能により Vector と書くだけで任意の要素型を密に格納した一次元配列を表す抽象型を表現できるようになります。

型の別名

既に表現できている型に対して新しい名前が付けられると便利な場合がありますが、これは単純な代入文で行えます。例えば、UInt はシステムのポインタサイズに応じて UInt32 または UInt64 のいずれかであるはずです:

# 32-bit システム:
julia> UInt
UInt32

# 64-bit システム:
julia> UInt
UInt64

この切り替えは /base/boot.jl の次のコードで実現されています:

if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

もちろんこれは Int がどんな型の別名かに依存しますが、この値は正しく (Int32 または Int64 に) 設定されます。

(Int と異なり、AbstractFloat のサイズを固定した Float という型の別名は存在しません。整数レジスタでは Int のサイズがマシンのネイティブなポインタのサイズとなるのに対して、浮動小数点数レジスタのサイズは IEEE 754 規格で規定されるためです。)

型に対する演算

Julia の型はそれ自身オブジェクトなので、通常の関数で型に対する操作が可能です。型の操作や検査で特に有用な関数のいくつかはこれまでに紹介しました。例えば <: 演算子は左辺が右辺の部分型かどうかを判定します。

isa 関数はあるオブジェクトが与えられた型を持つかどうかを判定し、true または false を返します:

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

マニュアルで何度も使ってきた typeof は引数の型を返します。上述の通り型もオブジェクトなので、型を持ちます。型の型を尋ねることが可能です:

julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,String})
Union

もう一度 typeof を適用したらどうなるでしょうか? 「型の型の型」は何でしょうか? Julia の型は全て複合型の値であり、必ず DataType が返ります:

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType の型は DataType であり、自身と等しくなります。

一部の型に適用できる演算として、与えられた型の上位型を返す supertype があります。曖昧でない上位型を持つのは被宣言型 (DataType) だけです:

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

UnionAll の上位型は UnionAll となる場合があります3:

julia> supertype(Type)
Any

julia> supertype(Array)
DenseArray{T,N} where N where T

型オブジェクト Union (および型でないオブジェクト) に supertype を適用すると MethodError が発生します:

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
[...]

独自型の出力の整形

ある型のインスタンスの出力方法をカスタマイズしたい場合がよくあります。これは show 関数をオーバーロードすると実現できます。例えば複素数を極座標形式で表現する型を定義したとします:

julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar

ここでは異なる Real 型を引数に受け取ってそれらを共通の型に昇格するコンストラクタを定義しています (参照: コンストラクタ, 型の変換と昇格)。PolarNumber のように動作させるには、当然このコンストラクタの他にも +, *, one, zero といったメソッドや昇格規則を定義する必要があります。デフォルトでは、この型の値は Polar{Float64}(3.0,4.0) のように型の名前とフィールドの値を簡単に示す形で出力されます。

出力を 3.0 * exp(4.0im) のような形式に変えるには、その形式を io オブジェクト (ファイル・端末・バッファなどを表すオブジェクト) に書き込む次のメソッドを定義します:

julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

Polar オブジェクトの出力をさらに細かく制御することもできます。例えば、REPL などの対話的な環境で使われる詳細な複数行に渡る出力形式と、他のオブジェクト (配列など) の一部として出力されるときや print 関数で使われるコンパクトな一行の出力形式の二つを用意したい場合もあるでしょう。デフォルトでは両方の場合で show(io, z) 関数が使われますが、第二引数に text/plain という MIME タイプを受け取る三引数の show 関数をオーバーロードすることで、オブジェクトの複数行に渡る出力形式を定義できます。この例を示します:

julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
           print(io, "Polar{$T} complex number:\n   ", z)

ここで二行目の print(..., z) は二引数の show(io, z) メソッドを呼び出します。このメソッドを定義すると出力は次のようになります:

julia> Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Array{Polar{Float64},1}:
 3.0 * exp(4.0im)
 4.0 * exp(5.3im)

Polar の配列に対しては一行形式の show(io, z) が使われています。正確に言うと REPL は入力された行の結果に対して display(z) を呼び出し、この関数のデフォルトの動作が show(stdout, MIME("text/plain"), z) を呼び出し、さらにこの関数のデフォルトの動作が show(stdout, z) になっています。ただし、新しいマルチメディアディスプレイハンドラを定義する場合を除いて、display メソッドを新しく定義するべきではありません (参照: マルチメディア IO)。

また、他の MIME タイプに対する show 関数を定義すれば、リッチな表示をサポートする IJulia などの環境でオブジェクトのリッチな表示 (HTML や画像など) を有効にできます。例えば HTML による上付き文字と斜体を加えた Polar の表示を定義するには次のようにします:

julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
           println(io, "<code>Polar{$T}</code> complex number: ",
                   z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")

こうすると HTML の表示をサポートする環境で Polar オブジェクトが自動的に HTML を使って表示されるようになります。手動で show 関数を呼んでも HTML 形式の出力を得られます:

julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>

この出力を HTML でレンダリングするとこうなります: Polar{Float64} complex number: 3.0 e4.0i

強制ではないもののなるべく従うべき慣習として「オブジェクトを一行で出力する show メソッドは出力対象のオブジェクトを作成する正当な Julia 式を出力するべき」とされています。ただし、一行の show メソッドの出力が中置演算子 (例えば上記の Polar に対する一行の show メソッドにおける * 演算子) を含むときは、出力が他のオブジェクトの一部となったときに正しくパースできなくなる可能性があることに注意が必要です。この例を示すために、Polar 型のインスタンスの二乗を表す式オブジェクト (参照: プログラムの表現) を作って出力してみます:

julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2

^ 演算子は * 演算子よりも優先度が高い (参照: 演算子の優先順位と結合性) ので、この出力は意味すべき式 a ^ 2 つまり (3.0 * exp(4.0im)) ^ 2 を意味しません。この問題を解決するには、式オブジェクトを出力するときに内部で呼ばれる Base.show_unquoted(io::IO, z::Polar, indent::Int, precedence::Int) メソッドの定義が必要です:

julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
           if Base.operator_precedence(:*) <= precedence
               print(io, "(")
               show(io, z)
               print(io, ")")
           else
               show(io, z)
           end
       end

julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)

このメソッドは呼び出し側の演算子の優先度が乗算以上であれば show の周りに括弧を加えます。この確認を加えることで、括弧が無くても正しくパースされる式では括弧が出力されなくなります。例えば :($a + 2):($a == 2) では括弧が必要ありません:

julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)

julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)

環境に応じて show メソッドの振る舞いに調整が必要なこともあります。環境に関するプロパティとラップされた IO ストリームを伝える IOContext 型 (IO 型の部分型) を使うとこれを行えます。例えば :compact プロパティが true のときは短い表現を出力し、false のとき (および存在しないとき) は長い表現を出力する show メソッドは次のように書けます:

julia> function Base.show(io::IO, z::Polar)
           if get(io, :compact, false)
               print(io, z.r, "ℯ", z.Θ, "im")
           else
               print(io, z.r, " * exp(", z.Θ, "im)")
           end
       end

渡された IO ストリームが :compact プロパティを持つ IOContext オブジェクトのとき、新しく加えた短い形式が使われます。具体的に言うと、複数の列を持つ配列を出力する (水平方向のスペースが限られる) ときに短い形式が使われます:

julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im

julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Array{Polar{Float64},2}:
 3.0ℯ4.0im  4.0ℯ5.3im

出力を調整するのによく使われるプロパティについては IOContext のドキュメントを参照してください。

値型

Julia では truefalse といったに対してディスパッチを行うことはできません。しかしパラメトリック型に対するディスパッチは可能であり、さらに Julia では「プレーンビット」な値 (型・シンボル・整数・浮動小数点数・タプルなど) を型パラメータに利用できます。分かりやすいのが配列を表す型 Array{T,N} で、この型のパラメータ T は型 (Float64 など) ですが、NInt の値です。

パラメータに値を取る独自型を定義したときは、パラメータの値を使ってその独自型に対するディスパッチを制御できます。手の込んだ階層構造を考えずにこの概念を説明するために、パラメトリック型 Val{x} とコンストラクタ Val(x) = Val{x}() を考えます。

Val は次のように定義されます:

julia> struct Val{x}
       end

julia> Val(x) = Val{x}()
Val

Val の実装はこれだけです。Julia の標準ライブラリには Val を受け取る関数がいくつかあり、次のようにすれば Val を受け取る関数を独自に定義できます:

julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)

julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)

julia> firstlast(Val(true))
"First"

julia> firstlast(Val(false))
"Last"

Julia の標準ライブラリと使い方を一致させるために、呼び出し側は Val の型ではなくインスタンスを渡べきです。つまり foo(Val{:bar}) ではなく foo(Val(:bar)) を使ってください。

Val のようなパラメトリック値型の使い方は非常に間違えやすいことに注意が必要です。ひどいときには性能が格段に落ちることもあります。具体的に言うと、上に示したようなコードを実際に書くことはないはずです。Val の適切な (そして不適切な) 使い方については、パフォーマンス Tips にある詳しい議論を参照してください。


  1. 「少ない」の意味は定数 MAX_UNION_SPLITTING で定義され、現在この値は 4 です。[return]

  2. 訳注: 英語版では Type{T} だけが唯一のシングルトン型であるかのように説明されているが、これは事実と異なるので説明を修正した。なおバージョン 1.6 のマニュアルでは修正されている (参照: コミット c6955)。[return]

  3. 訳注: 英語版に UnionAll に関する記述が無かったので追加した。[return]

広告