型
型システムは、従来、2 つの全く異なる陣営に分類されてきました。: 静的型システムと動的型システムです。静的型システムでは、 すべてのプログラムの式は、プログラムの実行前に算出可能な型を持つ必要があります。一方動的型システムでは、プログラムによって処理されるされる実際の値が使用可能になる実行時まで、型については何もわかりません。オブジェクト志向プログラミングをすれば、静的型付け言語でも、コンパイル時に正確な値の型が分からなくてもコードを記述でき、あるていど柔軟性は高くなります。複数の異なる型を操作できるコードを記述する機能は、ポリモーフィズムと呼ばれます。 従来の動的型付け言語のすべてのコードはポリモーフィックです: 型に制約が生じるのは、明示的な型検査を行う場合もしくは、オブジェクトが実行時に操作に対応できなくなった場合です。
Julia の型システムは動的ですが、特定の値が特定の型であることを明示することで、静的型システムの利点の一部を得ることができます。これは効率的なコードを生成する上で大いに助けになるのですが、さらに重要なことに、関数引数の型に対するメソッドディスパッチを言語と深く統合できるのです。メソッドディスパッチはメソッドで詳しく説明しますが、ここで説明する型システムに根ざしています。
型を省略した場合の Julia の既定の挙動では、値に対して任意の型が許容されます。したがって、型を明示的に使用しなくても、多くの便利な Julia 関数を記述できます。ただし、追加の表現力が必要な場合は、元の "型指定されていない" コードに対して明示的な型注釈を徐々に導入することは簡単です。型注釈の目的は主に3つあります。Julia の強力な多重ディスパッチ メカニズムを利用すること、人間にとって読みやすくすること、プログラマのエラーを補足できるようにすることです。
Julia を、型システムの言葉で説明するならば、動的で、公称的で、パラメトリック、といういことになります。ジェネリック型はパラメータ化でき、型同士が持つ階層的な関係は互換性のある構造によって暗示されるのではなく、明示的に宣言されます。Julia の型システムの特に特徴的な特徴の 1 つは、具象型が互いに互いのサブタイプにはできないということです。全ての具体型は全てファイナル(下の階層を持たない)で、具象型のサブタイプ(上の階層)は抽象型のみです。最初は過度な制限に見えるかもしれませんが、この制約は驚くほど欠点が少なく、多くの有益な結果を導きます。動作を継承できることは、構造を継承するよりもはるかに重要であり、その両方を継承すると、従来のオブジェクト指向言語のように大きな困難が生じます。前もって言及すべきJuliaの型システムの他の高レベルの側面は次のとおりです:
- オブジェクトか非オブジェクトかという区分はありません: Julia のすべての値は、単一の完全に接続された型グラフに属する型を持つ真のオブジェクトであり、すべてのノードは型として等しくファーストクラスです。
- 「コンパイル時の型」という考え方は意味がありません: 値が持つ型は、プログラムの実行中の実際の型だけです。静的コンパイルとポリモーフィズムの組み合わせで成り立つオブジェクト指向の言語においては、コンパイル時/実行中の型の区別が重要になり、実行中の型のことは「ランタイム型」と呼びます。
- 変数ではなく値だけが型を持ちます。変数は単に値にバインドされた名前です。
- 抽象型と具象型の両方を他の型でパラメータ化できます。また、シンボル、値でその型が
isbits
で true が返されるもの(本質的には、数値や真偽値などで、他のオブジェクトへのポインタを持たないC型やstruct
などに格納されているもの)、およびそれらのタプルなどによってパラメータ化することもできます。型パラメーターは、参照や制限をする必要がない場合には省略できます。
Julia の型システムは、強力で表現力豊かでありながら、明確で直感的で控えめであるように設計されています。 Julia プログラマの多くは、型を明示的に使用するコードを記述する必要性を感じないかもしれません。ただし、ある種のプログラミングでは、宣言された型を使用すると、より明確で、よりシンプルで、より速く、より堅牢になります。
型宣言
演算子 ::
を使用すると、プログラム内の式や変数に型注釈をつけることができます。これを行う理由は主に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型システムを単なるオブジェクト実装の寄せ集め以上のものにしているのです。
整数と浮動小数点数では、さまざまな具体的な数値を導入しました: Int8
、UInt8
、Int16
、UInt16
、Int32
、UInt32
、Int64
、[UInt64]
(@ref)、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
は、型グラフの頂点にあるため、一般的に 「トップ」 と呼ばれます。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
です。数の世界を整数と実数の表現に分けます。実数の表現には、もちろん浮動小数点型が含まれるのですが、有理数など他の型もあります。したがって、AbstractFloat
は Real
の真のサブタイプで、実数の浮動小数点表現のみを含みます。整数はさらにsigned
とunsigned
に細分されます。
<:
演算子は一般に"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 ビットより小さい値を宣言することはできません。
Bool
、Int8
および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
の定義で 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
異なる 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}
のインスタンスは、64 ビット値の即時ペアとしてコンパクトかつ効率的に表現できます;Point{Real}
のインスタンスは、Real
のインスタンスの任意のペアを保持できる必要があります。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
T
が Real
のサブタイプである 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
このように型が混在するケースを適切に処理するコンストラクター メソッドは定義できますが、後で コンストラクタに議論は譲ります。
パラメトリック抽象型
パラメトリック抽象型の宣言もほぼ同じ方法で、抽象型の一群に対して型宣言を行います:
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}
の表記で共変型のようなものを、Point{>: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
に対して、Pointy{T}
のサブタイプであるPoint{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
のようなパラメトリック抽象型の目的はなんでしょうか? Point
が対角線 x=y
上にあり、座標が1つあれば十分な場合における 点のような実装を考えてみると:
julia> struct DiagPoint{T} <: Pointy{T}
x::T
end
ここで、Point{Float64}
と DiagPoint{Float64}
は、抽象型Pointf{Float64}
の実装です。これは T
に他の取りうる型を選んでも同じです。これによって、Point
とDiagPoint
のどちらを実装するにしても Pointy
オブジェクトを共通のインターフェイスにするプログラミングが可能になります。これについての、完全な解説は、メソッドとディスパッチを導入する次の メソッド の章に譲ります。
どんな型でも、型パラメータがとりうるようにしてしまうと、意味を成さない場合があります。そのような状況では次のように、 T
の範囲を制限することができます :
julia> abstract type Pointy{T<:Real} end
この宣言では、T
が任意の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 つあります:
- タプル型は、任意の数のパラメータを持つことができます。
- タプル型は、そのパラメータと 共変 です:
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
にすることが可能です。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}
は、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})
が真を返すのは、A
とB
が同じオブジェクトで、そのオブジェクトが型である場合に限る、ということです。パラメータ無しの 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<:Number
は、Number
の配列でInt
を含むもの全てを指します。(T
は少なくとも Int
以上の大きさでなければいけません)。構文 where T>:Int
はまた、型変数の下限のみを指定しています。Array{>:Int}
は、Array{T} where T>:Int
と同等です。
where
式は入れ子にできるので、型変数の境界は外側の型変数を参照できます。 たとえば、Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real
は、次の2要素タプルを参照します: 第一要素は Real
のいずれか、第二要素は、 各要素が第一要素と同じ型を要素に持つ配列。
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 次元配列を定義します: 内側の配列のかたは T
で表されていますが、内側の配列を1つ抜き出してくるとその各要素の型は同じだということで、また別の内側の配列を取り出し来たとき、その要素は別の型の場合があります。 一方、型T2
は、内部配列の内側の配列のすべてが 1 次元配列の方が等しい 1次元配列の1 次元配列を定義します。 T2
は抽象型であり、例えばArray{Array{Int,1},1} <: T2
であるのに対して、T1
は具象型です。その結果、T1
はゼロ引数コンストラクタ a=T1()
で構築できますが、T2
ではできません。
関数定義構文の短い形式と同様に、このような型に名前を付ける便利な構文があります:
Vector{T} = Array{T,1}
これはconst Vector = 配列{T,1} T
と同等です。 Vector{Float64}
の書き込みは Array{Float64,1}
を書くのと同じで、包括型の Vector
は、要素の型に関係なく、2 番目のパラメーター (配列ディメンションの数) が 1 であるすべての Array
オブジェクトをインスタンスとして持ちます。パラメトリック型を常に完全に指定する必要がある言語では、これは特に役に立ちませんが、Julia では、Vector
と書くだけで、任意の要素型のすべての 1 次元の密な配列を含む抽象型を表現することができます。
型エイリアス
既に表現可能な型に新しい名前を導入すると便利な場合があります。 これは、単純な代入文で行うことができます。 たとえば、UInt
は、システム上のポインターのサイズに適した UInt32
または UInt64
のいずれかに別名です:
# 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
もちろん、これは Int
がInt32
または Int64
) のどちらの別名なのかで変わりますが、この別名は正しい方になるよう事前に定義されています。
(Int
とは異なり、Float
は特定のサイズの AbstractFloat
の型の別名が存在しないことに注意してください。Int
のサイズがそのマシン上のネイティブ ポインターのサイズを反映する整数レジスタとは異なり、浮動小数点レジスタ のサイズは IEEE-754 標準で規定されています。)
型に対する演算
Julia の型はそれ自体がオブジェクトであるため、通常の関数を作用させられます。 型の操作や探索に特に役立つ関数が既に導入されています。 <:
演算子などは、左手のオペランドが、右側のオペランドのサブタイプであるかどうかを示す演算子です。
isa
関数は、オブジェクトが指定された型であるかを検査し、真偽値を返します:
julia> isa(1, Int)
true
julia> isa(1, AbstractFloat)
false
typeof
関数は、すでにこのマニュアルを通して使われていますが、引数で与えられたオブジェクトの型を返します。前述のように、型はオブジェクトであるため、型も型を持ち、typeof
の引数として型を与えるおkとができます:
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,String})
Union
この操作をを繰り返したらどうなるでしょうか? 型の型の型は何でしょう? すでに見たように、型はすべて複合型の値なので、すべてDataType
型になります:
julia> typeof(DataType)
DataType
julia> typeof(Union)
DataType
DataType
は自身の型でもあります。
一部の型に適用することのある、もう 1 つの操作は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
型と同じ用に動作させるためには、他の多くのメソッドを定義する必要があるでしょう。例えば、+
、*
、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)
関数がどちらの場合も呼び出されますが、ユーザー定義の 別の 複数形式で表示するためには、3引数をとるshow
関数で、2番めの引数に text/plain
MIME タイプ (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
メソッドを定義すべきではありません。
さらに、IJulia などの環境で、オブジェクトのより豊かな表示(HTML、画像など)をするために、他の MIME タイプの show
メソッドを定義することもできます。 例えば、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 表示をサポートする環境では、 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 式を表示すべきです。上記の 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
演算子 ^
の優先順位は *
よりも高いため (演算子の優先順位と結合則を参照)、この出力は (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)
上記で定義したメソッドは、呼び出し元演算子の優先順位が乗算の優先順位以上である場合に、括弧を追加します。 この検査によって、括弧なしでも正しく解析する式 (:($a + 2)
や:($a =2)
など) では、括弧を省略して表示できます:
julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)
julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)
場合によっては、コンテキストに応じて show
メソッドの動作を調整すると便利です。これはIOContext
型で実現可能です。ラップされた IO ストリームと共にコンテキストプロパティを渡すことができます。 たとえば、:compact
プロパティが true
に設定されている場合は、show
メソッドで短い表現を作成し、false
または指定なしだと、長い表現になるというようなことができます:
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
などの値を関数ディスパッチに使用できません。ただし、パラメトリック型によるディスパッチは可能で、その型パラメータとして 「普通の」値 (型、シンボル、整数、浮動小数点数、タプルなど) を使うことができます。 一般的な例は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
typeを使用するのではなく、Val
のインスタンスを渡します。つまり、呼び方は、foo(Val{:bar})
ではなく foo(Val(:bar))
です。
Val
を含むパラメトリックな「値」型は、非常に誤用しやすいので注意して下さい。 ひどいときは、コードのパフォーマンスを大幅に低下させる可能性があります。 特に、上記のようなコードを、実用的に使いたいと思うことはないでしょう。 適切(および不適切)なVal
の使用方法野詳細については、パフォーマンス・ティップスの広範な議論を読んでください。
ここでいう、"少数" はMAX_UNION_SPLITTING
定数で定義され、現在 4 に設定されています。