型
伝統的に、プログラミング言語の型システムは大きく異なる二つの派閥に分類されます: プログラムに含まれる全ての式に対する型が実行に先立って計算できなければならない静的型システム (static type system) と、実行時に実際の値がプログラムによって計算されるまで型については何も分からない動的型システム (dynamic type system) です。オブジェクト指向を使うとコンパイル時に値の型が正確には決まらないコードを書けるようになるので、静的型付け言語にいくらかの柔軟性がもたらされます。異なる型に対して操作を行うコードが書ける機能を多相性 (polymorphism) と呼びます。古典的な動的型付け言語のコードは全て多相的です: 実行時に型を明示的に確認するときとオブジェクトが演算をサポートしていないときを除けば、任意の値の型は一切制限を受けません。
Julia の型システムは動的ですが、ある値が特定の型を持つことを表明できるようにすることで静的型システムの利点の一部を取り入れてもいます。この機能は効率的なコードを生成する上で大きな助けとなります。しかしさらに重要なのは、関数の型に応じたメソッドのディスパッチと言語の深い統合が可能になることです。メソッドのディスパッチはメソッドの章で詳細に説明しますが、ディスパッチを支えているのはこの章で説明する型システムです。
Julia で値の型を省略すると、その値は任意の型になれます。そのため Julia では型を全く明示しなくても様々な便利な関数を書くことができます。コードを書いた後に型を細かく表現する必要が生じたら、その "型無し" コードに少しずつ明示的な型注釈を加えていくことが可能です。注釈の追加には三つの目的があります: Julia の強力な多重ディスパッチ機構の活用、人間にとっての読みやすさの向上、そしてプログラマーのエラーの検出です。
型システムの言葉を使って説明すると、Julia の型システムは動的 (dynamic) で、名前的 (nominative) で、パラメトリック (parametric) です。総称型はパラメータ化が可能であり、型の間の階層関係は型の構造に互換性があるかどうかで推論されるのではなく明示的に宣言されます。Julia の型システムで特に変わっているのが、ある具象型が別の具象型の部分型になれないことです: 全ての具象型は final であり、上位型になれるのは抽象型だけです。一見これは過度な制限に思えるかもしれませんが、利点は様々あるのに対して欠点はほとんどありません。私たちの実感としては、構造が継承できることより振る舞いが継承できることの方がずっと重要であり、伝統的なオブジェクト指向言語ではこの両者を同時に継承するために大きな困難が生じています。
Julia の型システムが持つ高水準な特徴の中で最初に言及しておくべきものを示します:
- オブジェクトである値とオブジェクトでない値の区別はありません。Julia の全ての値は真のオブジェクトであり、完全に連結な単一の型グラフを持ちます。型グラフの全てのノードは全て等しくファーストクラスの型です。
- "コンパイル時型" という概念はありません。値が持つ唯一の型はプログラムが実行中にその値が持つ実際の型だけです。この型はオブジェクト指向言語で "実行時型" と呼ばれる概念です。オブジェクト指向言語では多相性を持った静的コンパイルがあるので、二つの型の区別が重要になります。
- 型を持つのは値だけであり、変数は値を持ちません ──変数は値に束縛されるただの名前です。
- 抽象型と具象型はどちらも他の型を使ってパラメータ化できます。パラメータ化には型の他にも
isbits
がtrue
を返す任意の型の値 (つまり C の型と同じように格納される数値や真偽値、および他のオブジェクトに対するポインタを持たないstruct
) やシンボル、さらにそれらのタプルが利用できます。参照されない型パラメータや制限を受けない型パラメータは省略可能です。
Julia の型システムは強力かつ高い表現力を持ち、そうでありながらも明快かつ直感的で控えめなように設計されています。多くの Julia プログラマーは型を明示的に使うコードを書く必要がないかもしれません。しかし一部の問題では、型宣言を使うとコードがより明快で、単純で、高速で、頑健になります。
型宣言
式や変数に型注釈を加えるには ::
演算子を使います。型注釈を使う主な目的は次の通りです:
- プログラムが思った通りに実行されることを確認するアサーションとして使う。
- 型に関する追加の情報をコンパイラに提供する。コンパイラはこの情報をもとに性能を向上させる場合がある。
値を計算する式に付いている ::
演算子の意味は「左辺は右辺のインスタンスである」であり、式の型を確認したいときにいつでも利用できます。右辺が具象型であれば、左辺の値はその型の実装である必要があります ──全ての具象型は 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
Number
は Any
の直接の子であり、Real
は Number
の直接の子です。Real
には二つの子 Integer
と AbstractFloat
があり、世界を整数の表現と実数の表現に分けています (Real
は他にも子を持ちますが、これは後で触れます)。実数の表現にはもちろん浮動小数点数型が含まれますが、有理数型なども含まれます。そのため AbstractFloat
は Real
の真の部分型となっており、AbstractFloat
には浮動小数点数を使った実数の表現だけが含まれます。Integer
はさらに Signed
と Unsigned
に分かれます。
<:
演算子は「...の部分型である」を意味します。上記のように型宣言で使うと、右辺の型が新しく作成される左辺の型の直接の上位型であることが宣言されます。式の中で部分型演算子としても利用でき、そうすると左辺が右辺の部分型であるとき 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
の違いは全て振る舞い ──これらの型のオブジェクトを引数に受け取った関数の動作── にあります。名前的な型システムが必要な理由はここにあります: もし構造が型を決めるとしたら、型が振る舞いを決めるので、Bool
と Int8
と UInt8
に異なる振る舞いをさせることが不可能になります。
複合型
複合型 (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
は任意の型で、Nothing
は nothing
を唯一のインスタンスとするシングルトン型です。これは他の言語で 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}
のインスタンスは 64 ビットの即値のペアとしてコンパクトかつ効率的に表現できます。- 一方で
Point{Real}
は任意のReal
のインスタンスのペアを保持できなければなりません。Real
のインスタンスとなるオブジェクトは任意のサイズと構造を持てることから、実装ではPoint{Real}
のインスタンスを個別にアロケートされた二つの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}
型は Point
の T
を Float64
に置き換えた具象型なので、コンストラクタもそのように利用できます:
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
オブジェクトの共通インターフェースを Point
と DiagPoint
に対して実装すれば、共通インターフェースに対するプログラミングが可能になります。しかしこれ以上のことを説明するには、次章のメソッドで説明されるメソッドとディスパッチに関する知識が必要です。
型パラメータが全ての型を取ってはいけない状況もあります。このような場合には 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
比は整数値に対して意味を持つので、型パラメータ T
は Integer
の部分型に制限されます。さらに整数の比は実数直線上の値を表すので、任意の Rational
が抽象型 Real
の部分型となるよう宣言されます。
タプル型
タプルは関数の引数の抽象化です ──関数の処理は含まれません。関数の引数の特徴的な性質は順序と型です。そのためタプル型はパラメータ化された不変型であり、一つのパラメータが一つのフィールドに対応します。例えば二要素のタプル型は次の不変型と基本的に同じです:
struct Tuple2{A,B}
a::A
b::B
end
ただし、この Tuple2
と実際のタプル型の間には三つの重要な違いがあります:
- タプルは任意の個数のパラメータを持てます。
- タプルはパラメータの型に関して共変 (covariant) です。例えば
Tuple{Int}
はTuple{Any}
の部分型となります。そのためTuple{Any}
は抽象型とみなされ、タプル型はパラメータが全て具象型であるときに限って具象型とみなされます。 - タプルはフィールド名を持ちません。フィールドには添え字でアクセスします。
タプルの値は括弧とコンマを使って書きます。タプルが構築されるとき、適切なタプル型がその場で生成されます:
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))
が真になるのは A
と B
が同じ型オブジェクトのときであり、かつそのときに限ります。パラメータが付かない 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
は通常の型ではないはずです。答えは「Ptr
は UnionAll
型と呼ばれる異なる種類の型である」となります。Array
のような他のパラメトリック型も同様に UnionAll
型であり、こういった型はパラメータが取り得る値それぞれに対する型を全て合併したものを表します。
通常 UnionAll
型はキーワード where
を使って書かれます。例えば Ptr
は Ptr{T} where T
と書いた方が正確であり、これは適当な T
を使って Ptr{T}
と書ける型からなる集合を表します。この文脈で T
は型を値に取る変数のようなものなので、「型変数 (type variable)」と呼ばれます。一つの where
が一つの型変数を導入し、複数のパラメータを持つ型には Array{T,N} where N where T
のように where
が複数付きます。
UnionAll
型 A
に対して 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<:Integer
は Integer
の部分型を要素とする配列を表します。また Array{<:Integer}
が Array{T} where T<:Integer
の省略形として提供されます。型変数は上限と下限を持つことができ、例えば Array{T} where Int<:T<:Number
は Number
の配列であって 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
型を引数に受け取ってそれらを共通の型に昇格するコンストラクタを定義しています (参照: コンストラクタ, 型の変換と昇格)。Polar
を Number
のように動作させるには、当然このコンストラクタの他にも +
, *
, 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 では true
や false
といった値に対してディスパッチを行うことはできません。しかしパラメトリック型に対するディスパッチは可能であり、さらに Julia では「プレーンビット」な値 (型・シンボル・整数・浮動小数点数・タプルなど) を型パラメータに利用できます。分かりやすいのが配列を表す型 Array{T,N}
で、この型のパラメータ T
は型 (Float64
など) ですが、N
は Int
の値です。
パラメータに値を取る独自型を定義したときは、パラメータの値を使ってその独自型に対するディスパッチを制御できます。手の込んだ階層構造を考えずにこの概念を説明するために、パラメトリック型 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 にある詳しい議論を参照してください。