`

型システムは従来全く異なる2つの陣営に分類されてきました。 静的型システムと動的型システムです。 静的型システムでは、プログラムのすべての式は、実行前に算出可能な型でなければなりません。 動的型システムでは、型に関する情報は、実行時、すなわち、プログラムが処理する実際の値が利用可能になる時まで 何もわかりません。 オブジェクト指向では、静的型言語でもある程度柔軟性があり、コンパイル時に判明する値の正確な型をコードに書かなくても構いません。 異なる型に対して操作可能なコードを書ける能力は多相性と呼ばれます。 古典的な動的型システムのすべてのコードは多相的です。 わざわざ型を検査したり、実行時にオブジェクトが操作に対応し損なわない限り、どんな値の型でも制限を受けません。

Juliaの型システムは動的ですが、値に対して型を指定できるようにすることで、静的型システムの利点を幾分か取り入れています。 これは、効率的なコードを生成するための大きな手助けとなりますが。より重要なのは、関数の引数の型に対してメソッド・ディスパッチ が可能となり、言語に深く統合されていることです。メソッド・ディスパッチについてはメソッドで詳細に探索していますが、 ここに書いている型システムに根差しています。

Juliaのデフォルトの挙動では、型を省略した場合は、値に対して任意の型が許容されます。 このため、多くの役に立つ関数を、わざわざ型の指定をしなくても、Juliaでは書くことができます。 しかし、必要に応じて、もとの"型のない"コードに徐々に明示的な型注釈をつけていくのは簡単です。 型注釈をつけるのは3つの目的があります。 Juliaの強力な多重ディスパッチのしくみを使うため、人間が読みやすくするため、プログラマーのエラーを捕捉するためです。

Juliaのことを型システムの言葉で記述すると、 動的で、公称的で、パラメータ化可能です。 汎化型は、パラメータづけが可能で、型同士の階層的な関係は、明示的に宣言し互換構造から推論するのではありません。 特にJuliaに固有の型システムの特徴は、具象型は互いに互いのサブタイプとはならないことです。 具象型はすべてファイナルで、そのスーパータイプとなるのは抽象型のみです。 はじめは、この制約が不当に厳しく思えるかも知れませんが、多くの利点があり、欠点はほとんどありません。 挙動を継承できるほうが、構造を継承できるよりも重要であり、共に継承しようとすると、 従来のオブジェクト指向言語では、深刻な困難が生じることがわかっています。 他に、前もって言及すべきJuliaの型システムの高水準な特徴をあげると、

Juliaの型システムは、強力かつ表現力豊かでありながら、明快かつ直観的で目立たぬよう設計されています。 多くのJuliaのプログラマは、わざわざ型を使ってコードを書く必要性をまったく感じないかもしれません。 しかし、ある種のプログラムでは、型を宣言すると、より明快で、単純で、速く、堅牢になります。

`

型宣言

::演算子はプログラム中の式や変数に型注釈をつけるために使われます。 これには、2つの理由があります。

  1. プログラムが想定通りに動いているかを確かめる、アサーションとして役立てる
  2. コンパイラに付加的な情報を伝えて、状況によってはパフォーマンスが上がるようする

::演算子が値を計算する式についている場合は、"is instance of"とよみます。 左側の式の値は右側の型のインスタンスであると主張するためにどこでも使えます。 右側の方が具象型の場合、左側の値はその具象型となる実装でなければなりません。 すべての具象型は、ファイナルであり、実装はの他の具象型のサブタイプとはならないことを思い出してください。 型が抽象型の場合、実装した値の型はその抽象型のサブタイプで構いません。 型がアサーションと異なる場合、例外が投げられ、合致する場合は左側の値を返します。

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got 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  # in a local declaration
x::Int8 = 10   # as the left-hand side of an assignment

そして、現在のスコープ全体に適用されます。宣言の前の部分にまでです。 今のところ、型宣言は、REPLなどのグローバルスコープでは使えません。 というのも、Juliaにはグローバルな定数型がまだ存在しないからです。

この宣言は、関数の定義にもつけることができます。

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 «name» end
abstract type «name» <: «supertype» end

abstract typeキーワードによって、新しい抽象型が«name»という名前で導入されます。 必要に応じて、この名前と<:と既存の型を続けると、新しく宣言した型はその型を"親"とするサブタイプであることを指定できます。

スーパータイプを書かない場合、デフォルトのスーパータイプはAnyになります。 Anyは事前に定義された型で、すべてのオブジェクトはAnyのインスタンスであり、すべての型はAnyのサブタイプとなります。 Anyは、型理論では、型のグラフの頂点にあるため、通常「トップ」と呼ばれます。 またJuliaには、Union{}と書かれる、事前に定義された「ボトム」の抽象型があり、型のグラフの最下位となります。 これは'Any'のちょうど逆で、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の直下の子の型です。Realはその子です。 次にRealには2つの子があります(もっとあありますがここでは2つのみを示します、他のものは後述します)。 IntegerAbstractFloatは、数の世界を整数の表現と実数の表現に分離します。 実数の表現には、浮動小数点型が当然ありますが、有理数などの他の型もあります。 したがって、AbstractFloatRealの真のサブタイプで、実数の中の浮動小数点数の表現しかありません。 整数はさらにSignedUnsignedに細分されます。

<:演算子は通常「のサブタイプである」という意味の言葉で、このように宣言で利用します。 右側の型が新しく宣言した型の直接のスーパータイプであるという宣言になります。 また、式の中でサブタイプ演算子としても利用可能で、左の被演算子が右の被演算子のサブタイプの時にtrueを返します。

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

抽象型の重要な用途に、具象型のデフォルトの実装を与えることがあります。 簡単な例を考えてみると、

function myplus(x,y)
    x+y
end

まず注意したい点は、上記の引数の宣言はx::Anyy::Anyと同等である点です。 この関数がmyplus(2,5)のように呼び出されると、myplusという名前で引数の合うメソッドから最も特化したメソッドが選択されます。 (多重ディスパッチに関するさらなる情報はメソッドを参照のこと)。

上記のメソッドより特化したメソッドが見当たらない場合、次にJuliaは内部でmyplusという名前のメソッドを定義しコンパイルします。 このメソッドは汎化関数に対して、引数2個がInt型のメソッドに特化したものです。 つまり、暗黙裡に定義とコンパイルが行われます。

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

そして、最終的にこの特化したメソッドが呼び出されます。

このように、抽象型を使うと、あとで多くの具象型と組み合わせた時にデフォルトのメソッドとなる汎化関数を書くことができます。 多重ディスパッチのおかげで、プログラマーはメソッドをデフォルトと特化したものとどちらを使うかを、完全に制御することができます。

特記すべき重要な点は、引数が抽象型の関数を使っても、パフォーマンスの劣ることは全くない点です。 これは、関数が呼び出される毎に、具象型の引数のタプルそれぞれに対してリコンパイルを行うからです。 (しかし関数の引数が抽象型のコンテナの場合は、パフォーマンス上の問題が起こるかもしれません。 パフォーマンス・ティップスを参照のこと。)

`

プリミティブ型

プリミティブ型は、データが普通のビットで構成される具象型です。 プリミティブ型の定番の例は、整数と浮動小数点数です。 ほとんどの言語とは異なり、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

ビット数は、その型が格納に何ビット必要とするかを示し、新しい型の名前に使われます。 プリミティブ型は、必要に応じてどのスーパータイプのサブタイプなのかを宣言することができます。 スーパータイプを省略すると、デフォルトでは、Anyがその型の直接のスーパータイプになります。 したがって、上記のBool 宣言は、ブール値の格納に8ビットを要し、Integer が直接のスーパータイプであることを意味しています 。 現在は、8ビットの倍数であるサイズのみが利用可能です。 したがって、ブール値に本当に必要なのは1ビットだけですが、8ビットより小さい宣言にはできません。

BoolInt8UInt8の型はすべて同一の表現です。 これらは8ビットのメモリの塊です。 しかし、Juliaの型システムは公称的であるため、同一の構造であっても互換性はありません。 これらの基本的な違いは、スーパータイプが異なることです。 Boolの直接のスーパータイプは IntegerInt8Signed、UnsignedUInt8Unsignedです。 その他すべてのBoolInt8UInt8の違いは、挙動に関することです。 挙動とは結局、引数として与えられたオブジェクトの型に対して、関数がどのように動作するように定義されているかということです。 これが公称的な型システムが必要な理由です。 もしも構造によって型が決定するならば、型の構造から挙動がそのまま決まってしまうので、BoolInt8UInt8と異なる挙動をとらせることは不可能になるでしょう。

`

複合型

複合型 は、レコード、構造体、オブジェクトなど、 言語によって様々な呼ばれ方をします。 複合型は、名前付きフィールドの集合体であり、そのインスタンスは単一の値のように扱うことができます。 多くの言語では、複合型はユーザーが定義できる唯一の型の種類であり、Juliaでも最も一般的に使われるユーザ定義型です。

C++、Java、Python、Rubyなどの主流のオブジェクト指向言語では、複合型に名前付き関数が関連づけられて、 その組み合わせは「オブジェクト」と呼ばれます。 RubyやSmalltalkのような、より純粋なオブジェクト指向言語では、すべての値は複合型であろうとなかろうとオブジェクトです。 少し不純なオブジェクト指向言語には、C++やJavaなどがあり、整数や浮動小数点数などの一部の値はオブジェクトではないですが、 ユーザーの定義する複合型のインスタンスは、真のオブジェクトで、関連づけられたメソッドを持ちます。 Juliaでは、すべての値がオブジェクトですが、関数は操作対象のオブジェクトとは関連づけられていません。 この仕様が必要なのは、Juliaでは、関数に対して使われるメソッドは、多重ディスパッチによって選択されるからです。 つまり、メソッドを選択するときには、すべての 関数の引数の型が考慮され、最初の引数のみではないからです(メソッドとディスパッチの詳細については、メソッドを参照してください)。 したがって、関数が最初の引数だけに「属する」のは不適切です。 各オブジェクトの "内側"にたくさんの名前付きのメソッドをいれるより、メソッド群を編成して関数オブジェクトにする方が、言語設計上、非常に有益です。

複合型は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

型は関数のように適用する時には、 コンストラクタ と呼ばれます。 2つのコンストラクタが自動的に生成されます(これらは デフォルトコンストラクタ と呼ばれます)。 1つはどんな引数でも許容し、convert を呼び出してフィールドの型に変換します。 もう1つはフィールドの型と完全に一致する引数だけを許容します。 2つのコンストラクタが生成されるのは、新しい定義を追加を簡単に、不注意でデフォルトのコンストラクタを置き換えることなく、 できるようにするためです。

barフィールドには型の制約はないので、値は何でも構いません。ただし、bazの値はIntに変換できる必要があります。

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(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で宣言された複合型オブジェクトは 不変 です。 生成後に変更することはできません。 これは最初は奇妙に思えるかもしれませんが、いくつかの利点があります:

不変オブジェクトは、配列などの可変なオブジェクトをフィールドとして含んでも構いません。 含まれているオブジェクトは可変のままです。 不変オブジェクトのフィールド自体が、別のオブジェクトを参照するように変更できなくなるだけです。

必要に応じて、可変な複合オブジェクトをキーワードmutable structで宣言することができます (次のセクションで検討します)。

フィールドのない複合型はシングルトンです。そのような型のインスタンスは1つしか作れません。

julia> struct NoFields
       end

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

===によって、NoFieldsの「2つの」生成されたインスタンスが、実際には同一であることを確認できます。 シングルトン型については、あとで で詳しく説明します。

複合型のインスタンスがどのように作成されるかについては、もっと多くの言うべきことがありますが、その議論はパラメータ型メソッドの両方もかかわり、十分重要なので、独自の章コンストラクタで解説します。

`

可変複合型

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

変更に対応できるように、このようなオブジェクトは、通常、ヒープ上に配置し、メモリアドレスは固定しています。 可変オブジェクトは、時間とともに値の変わりうる小さなコンテナと似ていて、アドレスだけで確実に識別できます。 対照的に、不変型のインスタンスは、特定のフィールド値に関連づけられています。 フィールド値だけで、オブジェクトに関するすべてがわかります。 型を可変にするかどうかを決めるには以下の問いを考えればいいでしょう。 同じフィールド値を持つ2つのインスタンスは同一だとみなせるか、あるいは時間とともに別々に変更する必要があるかと。 同一であると考えてもよいなら、おそらくその型は不変にすべきでしょう。

要約すると、2つの重要な特性がJuliaにおける普遍性を決定づけています。

`

宣言型

上記のセクションで説明した3種の型(抽象型、プリミティブ型、複合型)は、実のところ、すべて密接に関連しています。 これらは重要な特徴が共通しています。

特徴が共通しているため、これらの型は内部的に同じ概念のDataTypeのインスタンスとして表現されます。 DataTypeはこれらの型のいずれかのことです。

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

DataTypeは抽象型でも具象型でもかまいません。 具象型であれば、特定のサイズ、 格納領域の配置 があり、(場合によっては)フィールド名もあります。 そして、プリミティブ型は、サイズが0ではないDataTypeで、フィールド名を持ちません。 複合型は、フィールド名があるか、空(サイズ0)のDataTypeです。

システムのすべての具体的な値は、なんらかのDataTypeのインスタンスです。

`

合併型

合併型は特殊な抽象型で、この型にオブジェクトとして含まれるのは、引数のいずれかの型のインスタンスすべてであり、 特殊なキーワード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 Float64

多くの言語のコンパイラには、型推論のための内部でつかう合併構文があります。 Juliaは単にそれをプログラマにも公開しています。 型の数が少ない場合に 合併型 を使うと、Juliaのコンパイラは効率的なコードを生成します[1]。 なりうる型すべてに個別に特化したコードを生成します。

特に有益な合併型Union{T, Nothing}です。 ここでTは任意の型、Nothingは唯一のインスタンスがオブジェクトnothingだけのシングルトン型です。 Juliaのこのパターンは、他の言語の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の2つの「座標」を保持している新しいパラメータ型Point{T}を定義しています。 Tとはなんだ、と誰かが尋ねるかもしれません。これがまさしくパラメータ型のポイントです。 どんな型(実際にはプリミティブ型の値でも構いませんが、ここでは明らかに型が使われています)でもかまいません。 Point{Float64}は、Point の定義でTFloat64と置き換えたものと同等な具象型です。 よって、一つの宣言が実質的には、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

他の型は当然このサブタイプではありません。

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

異なるTの値がついた具象型Pointは決して互いにサブタイプとなることはありません。

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

julia> Point{Float64} <: Point{Real}
false
警告

この最後の点は 非常に 重要です。Float64 <: Realは成り立つにもかかわらず、Point{Float64} <: Point{Real}は 成り立ちません

型理論の術語で言い換えると、Juliaの型パラメータは covariant (or even contravariant)ではなく、不変 です。 これは現実的の理由によります。 {Float64}のインスタンスはPoint{Real} のインスタンスと概念的には似ているかもしれませんが、2つ型のメモリ内の表現は異なります。

Realのインスタンスとなるオブジェクトは任意のサイズや構造になりうるので、現実的には、Point{Real}のインスタンスは、個別に配置されたRealオブジェクトへのポインタの組として表現する必要があります。

Point{Float64}オブジェクトに、値を直接を格納できると、得られる効率は、配列の場合、非常に大きくなります。 Array{Float64}は、64ビットの浮動小数点数の連続したメモリブロックとして格納されますが、Array{Real}はそれぞれ別々に配置されたRealオブジェクトへのポインタの配列でなければなりません。 抽象型Realに宣言されたオブジェクトの実装は、64ビットの浮動小数点数がボックス化されていても構わないし、任意の大きさの複雑なオブジェクトでもかまいません。

Point{Float64}は、Point{Real}のサブタイプではないので、 以下のメソッドを型Point{Float64}の引数に適用することはできません。

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

TRealのサブタイプとなる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があります。(全合併型 を参照。)

より多くの例については、後の メソッド で説明します。

Pointオブジェクトはどのように構成するのでしょうか。 [コンストラクタ](@ ref man-constructor)で詳しく説明しますが、複合型にに対して独自のコンストラクタを定義することは可能ですが、特別にコンストラクタの宣言をしなくても、デフォルトで新しい複合型オブジェクトを作成する方法が2通りあります。1つは型パラメータを明示的に与えるもの、もう1つはオブジェクトコンストラクタへの引数から暗黙裡に推定されるものです。

Point{Float64}は、Tの代わりにFloat64 を使って宣言したPointと同等の具象型なので、Pointと同じようなコンストラクタとしてそのまま適用できます。

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

julia> typeof(ans)
Point{Float64}

デフォルトのコンストラクタには、各フィールドに対してちょうど1つ引数を指定する必要があります。

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)
[...]

パラメータ型に対しては、デフォルトのコンストラクタは1つしか生成されません。 オーバーライドできないためです。 このコンストラクタは任意の引数を受け取って、フィールドの型に変換します。

多くの場合、生成したいPointオブジェクトの型を指定するのは冗長です。 Pointコンストラクタを呼び出す際の引数に、すでに型情報が隠れているからです。 そのため、Pointのパラメータの型Tが推定可能で曖昧さがない場合は、Point自体をコンストラクタとして適用することも可能です。

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の場合、2つの引数が同じ型を持つ場合にのみ、型Tは明確に推定されます。 これ以外の場合、コンストラクタは失敗して、 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

このような型の混ざった場合でも適切に処理するコンストラクタメソッドは定義可能ですが、後述の[コンストラクタ](@ ref man-constructors)まで議論を保留します。

`

パラメータ抽象型

パラメータ抽象型に対しても、ほぼ同じ方法で、一群の抽象型に対して型宣言を行います。

julia> abstract type Pointy{T} end

この宣言では、Tは型や整数値を表し、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

Juliaでは、Pointy{<:Real}という表記で 共変型*** のようなもの、Pointy{>:Int}で **反変型 のようなものを表現できます。 しかし、技術的には、これらは型の集合を表しています。(全合併型 参照)

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 上にあるため、座標1つのみを必要とする点状のものを実装する場合を考えましょう。

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

ここでPoint{Float64}DiagPoint{Float64}は共に、抽象型Pointy{Float64}の実装で、これはTに他のとりうる型を選んでも同じです。 これによりPointDiagPointのどちらを実装するにも、Pointyオブジェクトを共通のインタフェースにするようなプログラミングが可能になります。 しかし、完全な解説は、メソッドとディスパッチを導入する次の章メソッド に持ち越します。

型のパラメータのとりうる型を自由にしてしまうと、意味を成さない場合があります。 そのような状況では、次のように、Tの範囲を制限することができます。

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

この宣言では、Tが任意のRealのサブタイプの場合で許容されますが、 Realのサブタイプでなければ許容されません。

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 Int64

パラメータ複合型の型パラメータも、同じ方法で制限できます。

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

実世界でパラメータ型という仕組みがどれほど役立つかという例として、 ここでは整数の比を表すRational という不変型を、Juliaで実際にどう定義するかを示します。 (単純化のため、ここではコンストラクタを省略します)

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

整数値の比率になる時だけ、意味をなすので、パラメータの型Tは、Integerのサブタイプに限定されています。 整数の比は数直線上の値を表現するので、任意のRational は、抽象型 Real のインスタンスです。

`

タプル型

タプルとは関数本体からその引数だけを抜き出したものです。 関数の引数の目立った特徴は、順序と型です。 そのため、タプル型は、不変なパラメータ複合型で各パラメータがフィールドの型に対応しているものと似ています。 たとえば、2要素のタプル型は、次の複合型に似ています。

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

ただし、3つの重要な違いがあります。

タプルの値は、括弧とカンマをつかって書きます。タプルが生成されると、必要に応じて適切なタプル型が生成されます。

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

直観的には、これは、関数の引数の型が関数のシグネチャのサブタイプであることに相当します(シグネチャが適合する場合)。

`

可変引数タプル型

タプル型の最後のパラメータは、特殊な型である可変引数にすることが可能で、任意個数の後続の要素を表します。

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}は、0個以上の型Tに対応することに注意してください。 可変引数タプル型は、可変引数メソッドによって受け入れられる引数を表すために使用されます(可変引数関数を参照)。

Vararg{T,N}は、ちょうどN個の型Tに対応します。 NTuple{N,T}Tuple{Vararg{T,N}}の便利なエイリアスです。 つまり、型Tの要素をちょうどN個含むタプル型です。

`

名前付きタプル型

名前付きタプル型は、 NamedTuple 型のインスタンスで、2つのパラメータを取ります。 シンボルのタプルはフィールド名を与え、型のタプルはフィールドの型を与えます。

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

NamedTuple型はコンストラクタとしても利用可能で、1個のタプルを引数としてとります。 生成されたNamedTupleの型は、両方のパラメータの指定された具象型か、フィールド名だけがしていされた型になります。

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

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

フィールドの型を指定した時は、引数は変換されます。 そうでない場合は、引数の型がそのまま使われます。

`

シングルトン型

ここで、特殊なパラメータ抽象型であるシングルトン型について触れておくべきでしょう。 型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のインスタンスです(もちろん、シングルトン型も含みます)。

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

パラメータメソッド変換の議論がすむまで、 シングルトン型がどう役に立つのかを説明するのは難しいですが、手短にいうと、関数の挙動を特定の型のだけに特化することができるのです。 これが役に立つのは、挙動が型に依存する(特にパラメトリックな)メソッドを書く時で、 しかもその型が勝手に推測されるのではなく、わざわざ引数として与える場合です。

Haskell、Scala、Rubyなどの人気のある言語には、シングルトン型が備わっています。 一般的な用法では、「シングルトン型」という術語は、唯一のインスタンスがで単一の値である型を指します。 この意味はJuliaのシングルトン型にも当てはまりますが、型オブジェクトだけがシングルトン型になるという点に注意してください。

`

パラメータプリミティブ型

プリミティブ型にもパラメータをつけて宣言することができます。 たとえば、ポインタはプリミティブ型として表現できて、Juliaでは以下のように宣言します。

# 32-bit system:
primitive type Ptr{T} 32 end

# 64-bit system:
primitive type Ptr{T} 64 end

一般的なパラメータ複合型と比べて、この宣言のちょっと変な特徴は、型パラメータTが型自体の定義に使われていないことです。 つまり、型パラメータは抽象的なタグであり、本質的に同一の構造である型の族全体をに定義し、型パラメータだけで差別化されています。 そのため、Ptr{Float64}Ptr{Int64}は、表現は同一であっても、型としては異なります。 もちろん、個別のポインタ型はすべて、包括型Ptrのサブタイプです。

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

`

全合併型

Ptrのようなパラメータ型はすべてのインスタンス(Ptr{Int64}など)のスーパータイプのように振る舞うと前に述べましました。 これはどのようにして実現しているのでしょうか? Ptr自体は通常のデータ型ではありえません。というのも、参照するデータの型が分からなければ、 明らかに、その型を記憶操作に使用できないからです。 答えは、Ptrの型(また他のArrayのようなパラメータ型)は、全合併型と呼ばれる種類の異なる型です 。 この型は、あるパラメータをすべての値に対して 繰り返し合併した 型を表現します。

全合併型は、通常、キーワードwhereを使って記述されます。 例えば、Ptrは、より正確にはPtr{T} where Tと書くことができて、あるTという値によってPtr{T}と書ける型をもつ値すべてを意味します。 この文脈では、パラメータTは型をまたぐ変数のようなものであるため、よく「型変数」と呼ばれます。 それぞれのwhereは型変数を一つ導入するため、こういった式は複数のパラメータを持つ場合、 例えばArray{T,N} where N where Tのように、型に対してネストします。

型の適用構文A{B,C}には、Aが全合併型であることが必要です。 まずAの一番外側の型変数をBで置換します。 その結果は別の全合併型になることと想定されているので、Cで置換します。 よってA{B,C}A{B}{C}は同等です。 これはArray{Float64}のように、型を部分的にインスタンス化することができる理由の説明となっています。 最初のパラメータの値は固定されていますが、2番目の値はすべてのとりうる値にまたがっているからです。 明示的にwhere構文を使用すると、どんなパラメータの部分集合にでも固定できます。 例えば、すべての1次元配列の型は、Array{T,1} where Tと書くことができます。

型変数は、サブタイプの関係をつかって制限することができます。 Array{T} where T<:Integerは、配列で要素の型がIntegerのいずれかになるものすべてを指しています。 構文Array{<:Integer}Array{T} where T<:Integerの便利な簡略表記です。 型変数は、下限と上限の両方を指定することができます。 Array{T} where Int<:T<:NumberNumberの配列でIntを含みうるものすべてを指します(少なくとも、TInt以上大きくなければなりません)。 構文where T>:Intはまた、型変数の下限のみを指定していて、 Array{>:Int}は、Array{T} where T>:Int同等です。

where式はネストする場合、型変数を限定する式は外側の型変数を参照することができます。 例えば、Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Realは、第1要素はRealのいずれかで、 第2要素は、各要素が第1要素の型を含む型の配列である配列の、2要素-タプルを参照します。

whereキーワード自体は、より複雑な宣言の内側でネストすることができます。 たとえば、次の宣言で作成される2つの型を考えてみましょう。

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は、1次元配列を要素とする、1次元配列を定義します。 内側の配列は同じ型のオブジェクトで構成されますが、この型は内側の配列ごとに異なる場合があります。 一方、型T2は、すべての内側の配列の型が等しい1次元配列の1次元配列を定義します。 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がインスタンスとして持つのは、2番目のパラメータ(配列の次元数)が1である、要素の種類に関係ないすべてのArrayオブジェクトです。 パラメータ型を常に完全に指定しなければならない言語では、こういう構文はそんなに有用ではないかもしれません。 しかしJuliaでは、Vectorと書くだけで、任意の要素型のすべての1次元の密な配列を含む抽象型を表すことができます。

`

型エイリアス

すでに表現可能な型に新しい名前をつけると便利な場合が時々あります。 これは簡単な代入文で行うことができます。 たとえば、UIntは、システム上のポインタのサイズによって、UInt32UInt64 のどちらかの別名となります。

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

これはbase/boot.jlの中にある以下のコードで実現できます。

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

もちろん、これは IntInt32Int64のどちらの別名なのかで変わりますが、 この別名は正しい型になるように事前に定義されています。

Intと違って、Floatという、AbstractFloatの特定のサイズの型の別名は存在しない点に注意してください。 整数レジスタとは異なり、浮動小数点レジスタのサイズは、IEEE-754規格で規定されています。 一方Intのサイズは、そのマシン上のネイティブポインタのサイズを反映しています。)

`

型に対する演算

Juliaの型はそれ自体がオブジェクトなので、通常の関数を作用させることができます。 特に型の操作や探索に役立つ関数がいくつか既に導入されています。 <:などは、左側の被演算子が右側の被演算子のサブタイプであるかどうかを示す演算子です。

isa 関数は、オブジェクトが指定された型であるかどうかを検査し、真か偽を返します。

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

typeof() 関数は、既にこのマニュアルを通して例の中で使っていますが、引数の型を返します。 上述のように、型はオブジェクトであり、それ自体の型もあるので、型に対してその型が何であるかを尋ねることができます。

julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,Float64,Rational})
DataType

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

この操作を繰り返すとどうなるでしょうか?型の型の型は何でしょうか? 既にみたように、型はすべて複合型の値なので、すべてDataType型になります。

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataTypeは自身の型でもあります。

ある種の型に対する別の演算には、supertype()があり、型のスーパータイプを示します。 宣言型(DataType)のみが明確なスーパータイプを持っています:

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

supertype() を他の型のオブジェクト(または、型ではないオブジェクト)に適用した場合は、MethodError が発生します。

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
  supertype(!Matched::DataType) at operators.jl:42
  supertype(!Matched::UnionAll) at operators.jl:47

`

独自の整形表示

型のインスタンスがどのように表示されるかを独自に指定したい場合がよくあります。 これは、show() 関数のオーバーロードによって可能です。 たとえば、極座標形式で複素数を表す型を定義するとします。

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

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

この例では独自のコンストラクタ関数を追加して、異なるReal型の引数をとると、それらを共通の型に昇格できるようにしました ( コンストラクタ変換と昇格を参照)。 (もちろん、Number型と同じように動作させるためには、他にも多くのメソッドを定義する必要があるでしょう (例えば、+*onezero、昇格のルールなど)。 デフォルトでは、この型のインスタンスの表示はかなり単純で、型名とフィールド値を知らせるだけなので、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などの対話環境で単一のオブジェクトを表示する場合に、簡単な単一行形式は、オブジェクトを別の(配列などの)オブジェクトの一部として表示する場合にと、両方を行いたい場合があります。 デフォルトでは、show(io, z)関数がどちらの場合にも呼び出されますが、ユーザーが定義した 別の 複数行の形式を表示することもできます。 そのためには、3引数のshow関数で、2番目の引数に、text/plainMIMEタイプ(Multimedia I/O参照)をとるものをオーバーロードします。例えば、

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

(ここでは、print(..., z)は、2引数の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)

単一行のshow(io, z)形式は、依然として、Polarの値の配列に使用されています。 技術的には、REPLが、display(z)を呼び出して、実行結果の一行を表示します。 冗長な複数行印刷形式は、show(STDOUT, MIME("text/plain"), z)が 、簡単な単一行形式は、show(STDOUT, z)がそれぞれデフォルトです。 しかし、新しいマルチメディアディスプレイハンドラを定義する場合を除いて(Multimedia I/O参照)、新たにdisplay()メソッドを定義 すべきではありません

さらに、showメソッドを他のMIMEタイプ向けに定義して、対応している環境(例えばIJulia)では、オブジェクトをよりリッチな表示(HTML、画像など)にすることもできます。 たとえば、Polarオブジェクトに対して書式付きのHTML表示を定義し、上付き文字と斜体を使うには、次のようにします。

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

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>

An HTML renderer would display this as: Polar{Float64} complex number: 3.0 e4.0 i

経験から言うと、単一行の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

^*より優先順位が高いため、([演算子の優先順位と結合則(@ref)参照])、この式はa^2を忠実に表現していません。 (3.0 * 1xp(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)

上で定義されたメソッドは、呼び出す演算子の優先順位が乗法以上の時に、括弧を付け足します。 この検査によって、括弧なしでも、正しく解析される時は省略して表示できます。

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

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

場合によっては、文脈によってshowメソッドの挙動を調整できると有益なことがあります。 これは、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

この新しい簡潔な表現は、:compactプロパティを持つIOContextをIOストリームとして渡した時に利用可能です。 特に、配列を何段かで表示し、(水平方向の幅が制限されている)時に役立ちます。

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のような を利用できません。 しかし、パラメータ型によるディスパッチは可能で、その型パラメータとして「普通の」値(型、シンボル、整数、浮動小数点数、タプルなど)を使うことができます。 よくある例としては、Array{T,N}で使われる次元のパラメータがあり、この場合Tは型( Float64など)ですが、NはただのInt型の値です。

値をパラメータとする独自の型を作成して、その型によってディスパッチを制御することができます。 この考え方を説明するために、パラメータ型のVal{T}を導入しましょう。 手の込んだ階層を必要としない時は、この手法にはこの型を慣用的に使います。

Val{T}は次のように定義します。

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 を使うのではなく、Valインスタンス を渡す必要があります。 つまり、foo(Val{:bar})ではなくfoo(Val(:bar))です。

Valを含めて、パラメトリックな「値」型は非常に誤用しやすい点に注意してください。 ひどい時には、コードのパフォーマンスを簡単に大幅に 悪化 させることもありえます。 特に、上述のようなコードを実用のコードとして書きたいとは決して思わないでしょう。 適切な(そして不適切な)Valの使い方の詳細については、the performance tipsの広範に渡る議論を読んでください。

[1]

"Small" は定数 MAX_UNION_SPLITTING で定義されており、現在は4に設定されています。