型システムは、従来、2 つの全く異なる陣営に分類されてきました。: 静的型システムと動的型システムです。静的型システムでは、 すべてのプログラムの式は、プログラムの実行前に算出可能な型を持つ必要があります。一方動的型システムでは、プログラムによって処理されるされる実際の値が使用可能になる実行時まで、型については何もわかりません。オブジェクト志向プログラミングをすれば、静的型付け言語でも、コンパイル時に正確な値の型が分からなくてもコードを記述でき、あるていど柔軟性は高くなります。複数の異なる型を操作できるコードを記述する機能は、ポリモーフィズムと呼ばれます。 従来の動的型付け言語のすべてのコードはポリモーフィックです: 型に制約が生じるのは、明示的な型検査を行う場合もしくは、オブジェクトが実行時に操作に対応できなくなった場合です。

Julia の型システムは動的ですが、特定の値が特定の型であることを明示することで、静的型システムの利点の一部を得ることができます。これは効率的なコードを生成する上で大いに助けになるのですが、さらに重要なことに、関数引数の型に対するメソッドディスパッチを言語と深く統合できるのです。メソッドディスパッチはメソッドで詳しく説明しますが、ここで説明する型システムに根ざしています。

型を省略した場合の Julia の既定の挙動では、値に対して任意の型が許容されます。したがって、型を明示的に使用しなくても、多くの便利な Julia 関数を記述できます。ただし、追加の表現力が必要な場合は、元の "型指定されていない" コードに対して明示的な型注釈を徐々に導入することは簡単です。型注釈の目的は主に3つあります。Julia の強力な多重ディスパッチ メカニズムを利用すること、人間にとって読みやすくすること、プログラマのエラーを補足できるようにすることです。

Julia を、型システムの言葉で説明するならば、動的で、公称的で、パラメトリック、といういことになります。ジェネリック型はパラメータ化でき、型同士が持つ階層的な関係は互換性のある構造によって暗示されるのではなく、明示的に宣言されます。Julia の型システムの特に特徴的な特徴の 1 つは、具象型が互いに互いのサブタイプにはできないということです。全ての具体型は全てファイナル(下の階層を持たない)で、具象型のサブタイプ(上の階層)は抽象型のみです。最初は過度な制限に見えるかもしれませんが、この制約は驚くほど欠点が少なく、多くの有益な結果を導きます。動作を継承できることは、構造を継承するよりもはるかに重要であり、その両方を継承すると、従来のオブジェクト指向言語のように大きな困難が生じます。前もって言及すべきJuliaの型システムの他の高レベルの側面は次のとおりです:

Julia の型システムは、強力で表現力豊かでありながら、明確で直感的で控えめであるように設計されています。 Julia プログラマの多くは、型を明示的に使用するコードを記述する必要性を感じないかもしれません。ただし、ある種のプログラミングでは、宣言された型を使用すると、より明確で、よりシンプルで、より速く、より堅牢になります。

型宣言

演算子 :: を使用すると、プログラム内の式や変数に型注釈をつけることができます。これを行う理由は主に2 つです:

  1. プログラムが期待どおりに動作することを確認するためのアサーションとして、
  2. コンパイラに追加の型情報を提供し、状況によってはパフォーマンスを向上できるようにすること

計算式に追加した::演算子は "is an 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

そして、宣言の前であっても、現在のスコープ全体に適用されます。現在、Julia には定数型グローバルがないため、型宣言は REPL などのグローバル スコープでは使用できません。

また、宣言を、関数定義に差し込むこともできます:

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

この関数の戻り値は、宣言された型の変数への代入と同じ様に処理されます。値は常にFloat64 に変換されます。

抽象型

抽象型はインスタンス化できず、型グラフ内のノードとしてのみ機能するだけですが、だからこそ関連する具象型の集合(抽象型の子孫である具象型)を説明できます。型の説明は抽象型から始めます。インスタンス化はできないですが、抽象型は型システムのバックボーンだからです。抽象型は概念的な(型の)階層を形成します。この階層が、Julia No型システムを単なるオブジェクト実装の寄せ集め以上のものにしているのです。

整数と浮動小数点数では、さまざまな具体的な数値を導入しました: Int8UInt8Int16UInt16Int32UInt32Int64、[UInt64](@ref)、Int128UInt128Float16Float32、およびFloat64です。表現のサイズは異なりますが、Int8Int16Int32Int64Int128 には、すべて整数型が符号付き型であるという共通点があります。同様に、UInt8UInt16UInt32UInt64およびUInt128は符号なし整数型であり、Float16Float32Float64は整数ではなく浮動小数点型とは異なります。たとえば、引数が何らかの整数である場合にのみ、コードの一部が意味をなすのが一般的ですが、実際には整数の特定の 種類 に依存しません。たとえば、最大公約数を求めるアルゴリズムは、あらゆる種類の整数に対して機能しますが、浮動小数点数では機能しません。抽象型を使用すると、型の階層を構築でき、具体的な型が適合するコンテキストを提供できます。これにより、たとえば、アルゴリズムを特定の整数に制限することなく、整数である任意の型に対してプログラミングすることが簡単にできます。

抽象型はabstract typeキーワードを使用して宣言されます。抽象型を宣言するための一般的な構文は次のとおりです:

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

abstract type キーワードは、名前が «name» で指定される新しい抽象型を導入します。この型名の後には、<: と 既存の型が続くことがあり、これは新しく宣言された抽象型が、"親"とするサブタイプであることを示します。

スーパータイプが指定されていない場合、デフォルトのスーパータイプは 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

Numberは「Any」の直接の子です。Realはそのさらに子に当たります。そして、Real には2人の子供がいます(本当はもっとありますが、ここでは2つだけ示します。他のものは後述します): 一つは Integer、もう一つはAbstractFloat です。数の世界を整数と実数の表現に分けます。実数の表現には、もちろん浮動小数点型が含まれるのですが、有理数など他の型もあります。したがって、AbstractFloatReal の真のサブタイプで、実数の浮動小数点表現のみを含みます。整数はさらにsignedunsigned に細分されます。

<: 演算子は一般に"is a subtype of" を意味しており、次のように宣言で使用します。演算子の右側の型が、新しく宣言した型のスーパータイプである、という宣言をします。また、式の中では サブタイプ演算子として利用することもでき、左のオペランドが、右のオペランドのサブタイプである場合に trueを返します:

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

抽象型の重要な使用方法に、具象型の既定の実装の提供があります。簡単な例として、以下を考えてみましょう:

function myplus(x,y)
    x+y
end

最初に注意すべき点は、上記の引数宣言は、 x::Any および y::Any としたのに相当する、ということです。この関数がmyplus(2,5)のように呼び出されると、ディスパッチャーは指定された引数に一致する myplusという名前の最も具体的なメソッドを選択します。(多重ディスパッチの詳細については、メソッドを参照してください。)

上記のメソッドより具体的なメソッドが見つからない場合、Julia は次に内部で myplusという名前のメソッドを定義しコンパイルします。この例では、定義・コンパイルされるメソッドは、上記のジェネリック関数から、引数2個をInt型に特化させたものです。つまり、定義とコンパイルは暗黙裡に行われます:

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

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

このように、抽象型を使用することでプログラマはジェネリック関数を書くことができます。これは後に、具象型を組み合わせて使う時の、既定のメソッドとして使うことができるのです。多重ディスパッチのおかげで、プログラマはデフォルトの関数を使うか、より特化したメソッドを使うかを完全に制御できます。

注意すべき重要な点は、プログラマが 引数が抽象型の引数を持つ関数使っても、パフォーマンスが低下しない、ということです。このメソッドが呼び出される毎に、具象型の引数のタプルそれぞれに対してメソッドが再コンパイルするからです。(ただし、関数の引数が抽象型のコンテナーであるは、パフォーマンス上の問題が発生するかもしれません パフォーマンス・ティップスを参照のこと。)

プリミティブ型

プリミティブ型は、データが普通ビットで構成される具象型です。プリミティブ型の典型的な例は、整数と浮動小数点の値です。ほとんどの言語とは異なり、Julia では、組み込みの決められたセットのみを提供するのではなく、独自のプリミティブ型を宣言できます。実際、標準のプリミティブ型はすべて(C言語ではなく)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 を直接のスーパータイプとして持っていることを意味します。現在、8 ビットの倍数のサイズのみがサポートされています。したがって、ブール値は実際には 1 ビットしか必要としませんが、8 ビットより小さい値を宣言することはできません。

BoolInt8およびUInt8の型はすべて同じ表現を持っています: どれも8ビットのメモリチャンクです。しかし、Juliaの型システムは公称的であるため、同一の構造であっても互換性はありません。これらの基本的な違いは、異なるスーパータイプを持っているということです:Boolの直接スーパータイプは[Integer](@ref)、[Int8](@ref)は[Signed](@ref)、UInt8(@ref)は[Unsigned](@ref)です。[Bool](@ref)、[Int8](@ref)、および[UInt8](@ref)のその他違いはすべて挙動に関するものです。これらの型のオブジェクトを引数として指定した場合に関数の挙動がどう定義されているかの違いです。もし、型の構造によって挙動が決まってしまうのであれば、[Int8](@ref)または[UInt8](@ref)とは異なる動作をする[Bool`](@ref)を作ることは不可能なるでしょう。

複合型

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

C++、Java、Python、Rubyなどの主流のオブジェクト指向言語では、複合型は、名前付けされた関数が関連付けられており、その組み合わせは "オブジェクト" と呼ばれます。Ruby や Smalltalk などの純粋なオブジェクト指向言語では、それが複合型であろうとなかろうと、すべての値がオブジェクトです。C++ や Java を含む純粋でないオブジェクト指向言語では、整数や浮動小数点値などの一部の値はオブジェクトではなく、ユーザー定義の複合型のインスタンスは真のオブジェクトで、関連付けられたメソッドを持ちます。Julia では、すべての値はオブジェクトですが、関数は操作対象のオブジェクトにバンドルされていません。これは、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 つはフィールドの型と完全に一致する引数だけを受け入れます。これらの両方が生成される理由は、誤って既定のコンストラクタを置き換えることなく、新しい定義を簡単に追加できるからです。

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 で宣言された複合型オブジェクトは 不変です。作成後に変更できません。これは最初は奇妙に見えるかもしれませんが、いくつかの利点があります:

不変オブジェクトには、配列などの変更可能なオブジェクトがフィールドに含まれている場合があります。含まれるオブジェクトは変更可能なままです。その不変オブジェクト自体フィールドが、異なるオブジェクトを指すよう変更することができなくなるだけです。

必要に応じて、次のセクションで説明するキーワード mutable struct で、変更可能な複合オブジェクトを宣言できます。

フィールドのない不変複合型はシングルトンです。このような型のインスタンスは 1 つだけです:

julia> struct NoFields
       end

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

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

複合型のインスタンスがどのように生成されるかについては、まだまだ説明すべきことがありますが、その議論は [パラメトリック型(@ref Parametric-types)とメソッドの両方にも関わりがあり、とても重要な事項なので、コンストラクタ という独立のセクションを設けてそこで扱うことにします。

可変複合型

複合型が、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つのインスタンスは同一だとみなせるか、あるいは時間とともに別々に変更する必要があるかを考えます。同一であるとみなせるならば、おそらくその型は不変にすべきでしょう。

まとめると、Julia では2 つの重要な特性で不変性を定義できます:

宣言型

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

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

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

DataTypeは抽象型、具象型どちらでもかまいません。具象型ならば、特定のサイズ、格納領域上のレイアウト、(場合によっては)フィールド名などがあります。プリミティブ型はゼロ以外のサイズの DataType ですが、フィールド名はありません。複合型は、フィールド名を持つか、、または空 (ゼロ サイズ)の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 のコンパイラは、少数の型 の Union 型 を使うと効率的なコードを生成することがあります[1]。なりうる型すべてに個別に特化したコードを生成するためです。

合併型のユースケースで特に便利なのは、Uniton{T, Nothing}です。ここでTは、任意の型を指定でき、Nothingは、唯一のインスタンスが nothingオブジェクトである、シングルトン型です。Julia のこのパターンは、他の言語のNullable, Option, Maybe 型などと同等です。関数の引数やフィールドをUniton{T, Nothing}として宣言すると型Tの値か、値がないことを示すnothingのどちらかに設定することができます。詳細については、FATのこの項目を見てください。

パラメトリック型

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

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

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 の型パラメータは、共変 (もしくは反変) ではなく、不変 です。これには現実的な理由があります: Point{Float64}のインスタンスはPoint{Real}のインスタンスと概念的には似ていますが、2つの型のメモリ上での表現は異なります:

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

Point{Float64}T の代わりに Float64 を使って宣言したPoint と同等の具象型であるため、それに応じてコンストラクタとしてそのまま適用できます:

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

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

多くの場合、生成しようとする 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

このように型が混在するケースを適切に処理するコンストラクター メソッドは定義できますが、後で コンストラクタに議論は譲ります。

パラメトリック抽象型

Parametric abstract type declarations declare a collection of abstract types, in much the same way:

julia> abstract type Pointy{T} end

With this declaration, Pointy{T} is a distinct abstract type for each type or integer value of T. As with parametric composite types, each such instance is a subtype of Pointy:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

Parametric abstract types are invariant, much as parametric composite types are:

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

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

The notation Pointy{<:Real} can be used to express the Julia analogue of a covariant type, while Pointy{>:Int} the analogue of a contravariant type, but technically these represent sets of types (see UnionAll Types).

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

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

Much as plain old abstract types serve to create a useful hierarchy of types over concrete types, parametric abstract types serve the same purpose with respect to parametric composite types. We could, for example, have declared Point{T} to be a subtype of Pointy{T} as follows:

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

Given such a declaration, for each choice of T, we have Point{T} as a subtype of Pointy{T}:

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

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

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

This relationship is also invariant:

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

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

What purpose do parametric abstract types like Pointy serve? Consider if we create a point-like implementation that only requires a single coordinate because the point is on the diagonal line x = y:

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

Now both Point{Float64} and DiagPoint{Float64} are implementations of the Pointy{Float64} abstraction, and similarly for every other possible choice of type T. This allows programming to a common interface shared by all Pointy objects, implemented for both Point and DiagPoint. This cannot be fully demonstrated, however, until we have introduced methods and dispatch in the next section, Methods.

There are situations where it may not make sense for type parameters to range freely over all possible types. In such situations, one can constrain the range of T like so:

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

With such a declaration, it is acceptable to use any type that is a subtype of Real in place of T, but not types that are not subtypes of 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

Type parameters for parametric composite types can be restricted in the same manner:

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

To give a real-world example of how all this parametric type machinery can be useful, here is the actual definition of Julia's Rational immutable type (except that we omit the constructor here for simplicity), representing an exact ratio of integers:

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

It only makes sense to take ratios of integer values, so the parameter type T is restricted to being a subtype of Integer, and a ratio of integers represents a value on the real number line, so any Rational is an instance of the Real abstraction.

Tuple Types

Tuples are an abstraction of the arguments of a function – without the function itself. The salient aspects of a function's arguments are their order and their types. Therefore a tuple type is similar to a parameterized immutable type where each parameter is the type of one field. For example, a 2-element tuple type resembles the following immutable type:

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

However, there are three key differences:

Tuple values are written with parentheses and commas. When a tuple is constructed, an appropriate tuple type is generated on demand:

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

Note the implications of covariance:

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

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

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

Intuitively, this corresponds to the type of a function's arguments being a subtype of the function's signature (when the signature matches).

Vararg Tuple Types

The last parameter of a tuple type can be the special type Vararg, which denotes any number of trailing elements:

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

Notice that Vararg{T} corresponds to zero or more elements of type T. Vararg tuple types are used to represent the arguments accepted by varargs methods (see Varargs Functions).

The type Vararg{T,N} corresponds to exactly N elements of type T. NTuple{N,T} is a convenient alias for Tuple{Vararg{T,N}}, i.e. a tuple type containing exactly N elements of type T.

Named Tuple Types

Named tuples are instances of the NamedTuple type, which has two parameters: a tuple of symbols giving the field names, and a tuple type giving the field types.

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

A NamedTuple type can be used as a constructor, accepting a single tuple argument. The constructed NamedTuple type can be either a concrete type, with both parameters specified, or a type that specifies only field names:

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

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

If field types are specified, the arguments are converted. Otherwise the types of the arguments are used directly.

Singleton Types

There is a special kind of abstract parametric type that must be mentioned here: singleton types. For each type, T, the "singleton type" Type{T} is an abstract type whose only instance is the object T. Since the definition is a little difficult to parse, let's look at some examples:

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

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

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

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

In other words, isa(A,Type{B}) is true if and only if A and B are the same object and that object is a type. Without the parameter, Type is simply an abstract type which has all type objects as its instances, including, of course, singleton types:

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

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

Any object that is not a type is not an instance of Type:

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

Until we discuss Parametric Methods and conversions, it is difficult to explain the utility of the singleton type construct, but in short, it allows one to specialize function behavior on specific type values. This is useful for writing methods (especially parametric ones) whose behavior depends on a type that is given as an explicit argument rather than implied by the type of one of its arguments.

A few popular languages have singleton types, including Haskell, Scala and Ruby. In general usage, the term "singleton type" refers to a type whose only instance is a single value. This meaning applies to Julia's singleton types, but with that caveat that only type objects have singleton types.

Parametric Primitive Types

Primitive types can also be declared parametrically. For example, pointers are represented as primitive types which would be declared in Julia like this:

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

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

The slightly odd feature of these declarations as compared to typical parametric composite types, is that the type parameter T is not used in the definition of the type itself – it is just an abstract tag, essentially defining an entire family of types with identical structure, differentiated only by their type parameter. Thus, Ptr{Float64} and Ptr{Int64} are distinct types, even though they have identical representations. And of course, all specific pointer types are subtypes of the umbrella Ptr type:

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

UnionAll Types

We have said that a parametric type like Ptr acts as a supertype of all its instances (Ptr{Int64} etc.). How does this work? Ptr itself cannot be a normal data type, since without knowing the type of the referenced data the type clearly cannot be used for memory operations. The answer is that Ptr (or other parametric types like Array) is a different kind of type called a UnionAll type. Such a type expresses the iterated union of types for all values of some parameter.

UnionAll types are usually written using the keyword where. For example Ptr could be more accurately written as Ptr{T} where T, meaning all values whose type is Ptr{T} for some value of T. In this context, the parameter T is also often called a "type variable" since it is like a variable that ranges over types. Each where introduces a single type variable, so these expressions are nested for types with multiple parameters, for example Array{T,N} where N where T.

The type application syntax A{B,C} requires A to be a UnionAll type, and first substitutes B for the outermost type variable in A. The result is expected to be another UnionAll type, into which C is then substituted. So A{B,C} is equivalent to A{B}{C}. This explains why it is possible to partially instantiate a type, as in Array{Float64}: the first parameter value has been fixed, but the second still ranges over all possible values. Using explicit where syntax, any subset of parameters can be fixed. For example, the type of all 1-dimensional arrays can be written as Array{T,1} where T.

Type variables can be restricted with subtype relations. Array{T} where T<:Integer refers to all arrays whose element type is some kind of Integer. The syntax Array{<:Integer} is a convenient shorthand for Array{T} where T<:Integer. Type variables can have both lower and upper bounds. Array{T} where Int<:T<:Number refers to all arrays of Numbers that are able to contain Ints (since T must be at least as big as Int). The syntax where T>:Int also works to specify only the lower bound of a type variable, and Array{>:Int} is equivalent to Array{T} where T>:Int.

Since where expressions nest, type variable bounds can refer to outer type variables. For example Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real refers to 2-tuples whose first element is some Real, and whose second element is an Array of any kind of array whose element type contains the type of the first tuple element.

The where keyword itself can be nested inside a more complex declaration. For example, consider the two types created by the following declarations:

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

Type T1 defines a 1-dimensional array of 1-dimensional arrays; each of the inner arrays consists of objects of the same type, but this type may vary from one inner array to the next. On the other hand, type T2 defines a 1-dimensional array of 1-dimensional arrays all of whose inner arrays must have the same type. Note that T2 is an abstract type, e.g., Array{Array{Int,1},1} <: T2, whereas T1 is a concrete type. As a consequence, T1 can be constructed with a zero-argument constructor a=T1() but T2 cannot.

There is a convenient syntax for naming such types, similar to the short form of function definition syntax:

Vector{T} = Array{T,1}

This is equivalent to const Vector = Array{T,1} where T. Writing Vector{Float64} is equivalent to writing Array{Float64,1}, and the umbrella type Vector has as instances all Array objects where the second parameter – the number of array dimensions – is 1, regardless of what the element type is. In languages where parametric types must always be specified in full, this is not especially helpful, but in Julia, this allows one to write just Vector for the abstract type including all one-dimensional dense arrays of any element type.

Type Aliases

Sometimes it is convenient to introduce a new name for an already expressible type. This can be done with a simple assignment statement. For example, UInt is aliased to either UInt32 or UInt64 as is appropriate for the size of pointers on the system:

# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

This is accomplished via the following code in base/boot.jl:

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

Of course, this depends on what Int is aliased to – but that is predefined to be the correct type – either Int32 or Int64.

(Note that unlike Int, Float does not exist as a type alias for a specific sized AbstractFloat. Unlike with integer registers, where the size of Int reflects the size of a native pointer on that machine, the floating point register sizes are specified by the IEEE-754 standard.)

Operations on Types

Since types in Julia are themselves objects, ordinary functions can operate on them. Some functions that are particularly useful for working with or exploring types have already been introduced, such as the <: operator, which indicates whether its left hand operand is a subtype of its right hand operand.

The isa function tests if an object is of a given type and returns true or false:

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

The typeof function, already used throughout the manual in examples, returns the type of its argument. Since, as noted above, types are objects, they also have types, and we can ask what their types are:

julia> typeof(Rational{Int})
DataType

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

What if we repeat the process? What is the type of a type of a type? As it happens, types are all composite values and thus all have a type of DataType:

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType is its own type.

Another operation that applies to some types is supertype, which reveals a type's supertype. Only declared types (DataType) have unambiguous supertypes:

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

If you apply supertype to other type objects (or non-type objects), a MethodError is raised:

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

Custom pretty-printing

Often, one wants to customize how instances of a type are displayed. This is accomplished by overloading the show function. For example, suppose we define a type to represent complex numbers in polar form:

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

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

Here, we've added a custom constructor function so that it can take arguments of different Real types and promote them to a common type (see Constructors and Conversion and Promotion). (Of course, we would have to define lots of other methods, too, to make it act like a Number, e.g. +, *, one, zero, promotion rules and so on.) By default, instances of this type display rather simply, with information about the type name and the field values, as e.g. 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)")

More fine-grained control over display of Polar objects is possible. In particular, sometimes one wants both a verbose multi-line printing format, used for displaying a single object in the REPL and other interactive environments, and also a more compact single-line format used for print or for displaying the object as part of another object (e.g. in an array). Although by default the show(io, z) function is called in both cases, you can define a different multi-line format for displaying an object by overloading a three-argument form of show that takes the text/plain MIME type as its second argument (see Multimedia I/O), for example:

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

(Note that print(..., z) here will call the 2-argument show(io, z) method.) This results in:

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)

where the single-line show(io, z) form is still used for an array of Polar values. Technically, the REPL calls display(z) to display the result of executing a line, which defaults to show(stdout, MIME("text/plain"), z), which in turn defaults to show(stdout, z), but you should not define new display methods unless you are defining a new multimedia display handler (see Multimedia I/O).

Moreover, you can also define show methods for other MIME types in order to enable richer display (HTML, images, etcetera) of objects in environments that support this (e.g. IJulia). For example, we can define formatted HTML display of Polar objects, with superscripts and italics, via:

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

A Polar object will then display automatically using HTML in an environment that supports HTML display, but you can call show manually to get HTML output if you want:

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

As a rule of thumb, the single-line show method should print a valid Julia expression for creating the shown object. When this show method contains infix operators, such as the multiplication operator (*) in our single-line show method for Polar above, it may not parse correctly when printed as part of another object. To see this, consider the expression object (see Program representation) which takes the square of a specific instance of our Polar type:

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

演算子 ^ の優先順位は * よりも高いため (演算子の優先順位と結合則を参照してください)、この出力は (3.0 * exp(4.0im)) ^ 2 と等しいはずの式 a ^ 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)

The method defined above adds parentheses around the call to show when the precedence of the calling operator is higher than or equal to the precedence of multiplication. This check allows expressions which parse correctly without the parentheses (such as :($a + 2) and :($a == 2)) to omit them when printing:

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

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

In some cases, it is useful to adjust the behavior of show methods depending on the context. This can be achieved via the IOContext type, which allows passing contextual properties together with a wrapped IO stream. For example, we can build a shorter representation in our show method when the :compact property is set to true, falling back to the long representation if the property is false or absent:

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

This new compact representation will be used when the passed IO stream is an IOContext object with the :compact property set. In particular, this is the case when printing arrays with multiple columns (where horizontal space is limited):

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

See the IOContext documentation for a list of common properties which can be used to adjust printing.

"値型"

In Julia, you can't dispatch on a value such as true or false. However, you can dispatch on parametric types, and Julia allows you to include "plain bits" values (Types, Symbols, Integers, floating-point numbers, tuples, etc.) as type parameters. A common example is the dimensionality parameter in Array{T,N}, where T is a type (e.g., Float64) but N is just an Int.

You can create your own custom types that take values as parameters, and use them to control dispatch of custom types. By way of illustration of this idea, let's introduce a parametric type, Val{x}, and a constructor Val(x) = Val{x}(), which serves as a customary way to exploit this technique for cases where you don't need a more elaborate hierarchy.

Val is defined as:

julia> struct Val{x}
       end

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

There is no more to the implementation of Val than this. Some functions in Julia's standard library accept Val instances as arguments, and you can also use it to write your own functions. For example:

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"

For consistency across Julia, the call site should always pass a Valinstance rather than using a type, i.e., use foo(Val(:bar)) rather than foo(Val{:bar}).

It's worth noting that it's extremely easy to mis-use parametric "value" types, including Val; in unfavorable cases, you can easily end up making the performance of your code much worse. In particular, you would never want to write actual code as illustrated above. For more information about the proper (and improper) uses of Val, please read the more extensive discussion in the performance tips.

[1]

ここでいう、"少数" はMAX_UNION_SPLITTING定数で定義され、現在 4 に設定されています。