型
型システムは従来全く異なる2つの陣営に分類されてきました。 静的型システムと動的型システムです。 静的型システムでは、プログラムのすべての式は、実行前に算出可能な型でなければなりません。 動的型システムでは、型に関する情報は、実行時、すなわち、プログラムが処理する実際の値が利用可能になる時まで 何もわかりません。 オブジェクト指向では、静的型言語でもある程度柔軟性があり、コンパイル時に判明する値の正確な型をコードに書かなくても構いません。 異なる型に対して操作可能なコードを書ける能力は多相性と呼ばれます。 古典的な動的型システムのすべてのコードは多相的です。 わざわざ型を検査したり、実行時にオブジェクトが操作に対応し損なわない限り、どんな値の型でも制限を受けません。
Juliaの型システムは動的ですが、値に対して型を指定できるようにすることで、静的型システムの利点を幾分か取り入れています。 これは、効率的なコードを生成するための大きな手助けとなりますが。より重要なのは、関数の引数の型に対してメソッド・ディスパッチ が可能となり、言語に深く統合されていることです。メソッド・ディスパッチについてはメソッドで詳細に探索していますが、 ここに書いている型システムに根差しています。
Juliaのデフォルトの挙動では、型を省略した場合は、値に対して任意の型が許容されます。 このため、多くの役に立つ関数を、わざわざ型の指定をしなくても、Juliaでは書くことができます。 しかし、必要に応じて、もとの"型のない"コードに徐々に明示的な型注釈をつけていくのは簡単です。 型注釈をつけるのは3つの目的があります。 Juliaの強力な多重ディスパッチのしくみを使うため、人間が読みやすくするため、プログラマーのエラーを捕捉するためです。
Juliaのことを型システムの言葉で記述すると、 動的で、公称的で、パラメータ化可能です。 汎化型は、パラメータづけが可能で、型同士の階層的な関係は、明示的に宣言し、互換構造から推論するのではありません。 特にJuliaに固有の型システムの特徴は、具象型は互いに互いのサブタイプとはならないことです。 具象型はすべてファイナルで、そのスーパータイプとなるのは抽象型のみです。 はじめは、この制約が不当に厳しく思えるかも知れませんが、多くの利点があり、欠点はほとんどありません。 挙動を継承できるほうが、構造を継承できるよりも重要であり、共に継承しようとすると、 従来のオブジェクト指向言語では、深刻な困難が生じることがわかっています。 他に、前もって言及すべきJuliaの型システムの高水準な特徴をあげると、
- 値にオブジェクトと非オブジェクトの区別はありません。 Juliaではすべて値は真のオブジェクトであり、オブジェクトはたった一つの型に属し、型どうしはすべてグラフの中で繋がっていて、 どのノードも等しく型として第一級です。
- 「コンパイル時の型」という考え方に意味はありません。値は唯一実行時の実際の型を持つのみです。 これは、オブジェクト指向言語では「実行時の型」と呼ばれ、静的コンパイルで多相性を実現するためにこの型の違いは重要です。
- 型を持つのは変数ではなく値のみです。 変数は値に束縛された単なる名前です。
- 抽象型と具象型は共に他の型によってパラメータ化可能です。 また他にも、記号・
isbits
が真になる型(本質的に、数やブール値など、他のオブジェクトへのポインタ持たないC言語の型や構造体に格納されるもの)やそれらのタプルによってもパラメータ化が可能です。 型のパラメータは参照や制限が必要ない時は省略可能です。
Juliaの型システムは、強力かつ表現力豊かでありながら、明快かつ直観的で目立たぬよう設計されています。 多くのJuliaのプログラマは、わざわざ型を使ってコードを書く必要性をまったく感じないかもしれません。 しかし、ある種のプログラムでは、型を宣言すると、より明快で、単純で、速く、堅牢になります。
型宣言
::
演算子はプログラム中の式や変数に型注釈をつけるために使われます。 これには、2つの理由があります。
- プログラムが想定通りに動いているかを確かめる、アサーションとして役立てる
- コンパイラに付加的な情報を伝えて、状況によってはパフォーマンスが上がるようする
::
演算子が値を計算する式についている場合は、"is instance of"とよみます。 左側の式の値は右側の型のインスタンスであると主張するためにどこでも使えます。 右側の方が具象型の場合、左側の値はその具象型となる実装でなければなりません。 すべての具象型は、ファイナルであり、実装はの他の具象型のサブタイプとはならないことを思い出してください。 型が抽象型の場合、実装した値の型はその抽象型のサブタイプで構いません。 型がアサーションと異なる場合、例外が投げられ、合致する場合は左側の値を返します。
julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got Int64
julia> (1+2)::Int
3
このため、型アサーションを任意の式にその場でつけることができます。
代入の左辺の変数に付け加える時や、local
宣言の一部である時、::
演算子の意味は少し違います。 これは、変数は常に指定した型であるという宣言となり、C言語のような静的型付き言語と同様です。 この変数に代入した値は,宣言した型に convert
を利用して変換されます。
julia> function foo()
x::Int8 = 100
x
end
foo (generic function with 1 method)
julia> foo()
100
julia> typeof(ans)
Int8
この機能は、代入によって変数の型が図らずも変更された時におこりうる、パフォーマンスの「落とし穴」を避けるために役立ちます。
この「宣言」の挙動は、特定のコンテキストでのみ発生します。
local x::Int8 # in a local declaration
x::Int8 = 10 # as the left-hand side of an assignment
そして、現在のスコープ全体に適用されます。宣言の前の部分にまでです。 今のところ、型宣言は、REPLなどのグローバルスコープでは使えません。 というのも、Juliaにはグローバルな定数型がまだ存在しないからです。
この宣言は、関数の定義にもつけることができます。
function sinc(x)::Float64
if x == 0
return 1
end
return sin(pi*x)/(pi*x)
end
この関数が終了すると、宣言した型で変数に代入するだけのようにふるまいます。 この値は常にFloat64
に変換されます。
抽象型
抽象型はインスタンス化できません。 型のグラフの中ではノードの役割を果たすだけですが、だからこそ関連する具象型の集合をその抽象型の子孫として記述することができます。 まず抽象型の話から始めることにします。というのも、インスタンス化はできないですが、型システムの骨格となるからです。 抽象型が概念的な階層を形成し、Juliaの型システムを単なるオブジェクトの寄せ集め以上のものにしているのです。
整数と浮動小数点数で様々な数値の具象型を導入したのを思い出してください。 Int8
, UInt8
, Int16
, UInt16
, Int32
, UInt32
, Int64
, UInt64
, Int128
, UInt128
, Float16
, Float32
, Float64
などです。 表現のサイズは異なりますが、Int8
, Int16
, Int32
, Int64
,Int128
はすべて符号付き整数型で、 UInt8
, UInt16
, UInt32
,UInt64
,UInt128
はすべて符号なし整数型であり、 Float16
, Float32
,Float64
は整数とは別の浮動小数点数型です。 コードは例えば、引数を整数の種類を限定して定義した場合、実はその 種類 にかかわらず動作することがよくあります。 最大公約数を求めるアルゴリズムは、すべての整数に対して動作しますが、浮動小数点数では、動作しません。 抽象型を使うと、型の階層を形成して、具象型の適合する文脈を作ることができます。 これによって、例えば、任意の整数型でに対するプログラムを簡単に、アルゴリズムを特定の整数型に制限することなく、作成することができます。
抽象型は abstract type
キーワードを使って宣言することができます。 抽象型を宣言する一般的な構文は、
abstract type «name» end
abstract type «name» <: «supertype» end
abstract type
キーワードによって、新しい抽象型が«name»
という名前で導入されます。 必要に応じて、この名前と<:
と既存の型を続けると、新しく宣言した型はその型を"親"とするサブタイプであることを指定できます。
スーパータイプを書かない場合、デフォルトのスーパータイプはAny
になります。 Any
は事前に定義された型で、すべてのオブジェクトはAny
のインスタンスであり、すべての型はAny
のサブタイプとなります。 Any
は、型理論では、型のグラフの頂点にあるため、通常「トップ」と呼ばれます。 またJuliaには、Union{}
と書かれる、事前に定義された「ボトム」の抽象型があり、型のグラフの最下位となります。 これは'Any'のちょうど逆で、Union{}
のインスタンスとなるオブジェクトは存在せず、すべての型がUnion{}
のスーパータイプです。
Juliaにおいて数の階層を形成する抽象型をいくつか考察しましょう。
abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer end
Number
はAny
の直下の子の型です。Real
はその子です。 次にReal
には2つの子があります(もっとあありますがここでは2つのみを示します、他のものは後述します)。 Integer
とAbstractFloat
は、数の世界を整数の表現と実数の表現に分離します。 実数の表現には、浮動小数点型が当然ありますが、有理数などの他の型もあります。 したがって、AbstractFloat
はReal
の真のサブタイプで、実数の中の浮動小数点数の表現しかありません。 整数はさらにSigned
とUnsigned
に細分されます。
<:
演算子は通常「のサブタイプである」という意味の言葉で、このように宣言で利用します。 右側の型が新しく宣言した型の直接のスーパータイプであるという宣言になります。 また、式の中でサブタイプ演算子としても利用可能で、左の被演算子が右の被演算子のサブタイプの時に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では組込みの決まったプリミティブ型が利用可能なだけではなく、 独自のプリミティブ型を宣言することができます。 実際、組込みのプリミティブ型はすべてJulia自体で定義されています。
primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end
primitive type Int8 <: Signed 8 end
primitive type UInt8 <: Unsigned 8 end
primitive type Int16 <: Signed 16 end
primitive type UInt16 <: Unsigned 16 end
primitive type Int32 <: Signed 32 end
primitive type UInt32 <: Unsigned 32 end
primitive type Int64 <: Signed 64 end
primitive type UInt64 <: Unsigned 64 end
primitive type Int128 <: Signed 128 end
primitive type UInt128 <: Unsigned 128 end
プリミティブ型を宣言する一般的な構文は、
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end
ビット数は、その型が格納に何ビット必要とするかを示し、新しい型の名前に使われます。 プリミティブ型は、必要に応じてどのスーパータイプのサブタイプなのかを宣言することができます。 スーパータイプを省略すると、デフォルトでは、Anyがその型の直接のスーパータイプになります。 したがって、上記のBool 宣言は、ブール値の格納に8ビットを要し、Integer が直接のスーパータイプであることを意味しています 。 現在は、8ビットの倍数であるサイズのみが利用可能です。 したがって、ブール値に本当に必要なのは1ビットだけですが、8ビットより小さい宣言にはできません。
Bool
、Int8
、UInt8
の型はすべて同一の表現です。 これらは8ビットのメモリの塊です。 しかし、Juliaの型システムは公称的であるため、同一の構造であっても互換性はありません。 これらの基本的な違いは、スーパータイプが異なることです。 Bool
の直接のスーパータイプは Integer
、Int8
はSigned
、UnsignedUInt8
はUnsigned
です。 その他すべてのBool
、Int8
、UInt8
の違いは、挙動に関することです。 挙動とは結局、引数として与えられたオブジェクトの型に対して、関数がどのように動作するように定義されているかということです。 これが公称的な型システムが必要な理由です。 もしも構造によって型が決定するならば、型の構造から挙動がそのまま決まってしまうので、Bool
をInt8
やUInt8
と異なる挙動をとらせることは不可能になるでしょう。
複合型
複合型 は、レコード、構造体、オブジェクトなど、 言語によって様々な呼ばれ方をします。 複合型は、名前付きフィールドの集合体であり、そのインスタンスは単一の値のように扱うことができます。 多くの言語では、複合型はユーザーが定義できる唯一の型の種類であり、Juliaでも最も一般的に使われるユーザ定義型です。
C++、Java、Python、Rubyなどの主流のオブジェクト指向言語では、複合型に名前付き関数が関連づけられて、 その組み合わせは「オブジェクト」と呼ばれます。 RubyやSmalltalkのような、より純粋なオブジェクト指向言語では、すべての値は複合型であろうとなかろうとオブジェクトです。 少し不純なオブジェクト指向言語には、C++やJavaなどがあり、整数や浮動小数点数などの一部の値はオブジェクトではないですが、 ユーザーの定義する複合型のインスタンスは、真のオブジェクトで、関連づけられたメソッドを持ちます。 Juliaでは、すべての値がオブジェクトですが、関数は操作対象のオブジェクトとは関連づけられていません。 この仕様が必要なのは、Juliaでは、関数に対して使われるメソッドは、多重ディスパッチによって選択されるからです。 つまり、メソッドを選択するときには、すべての 関数の引数の型が考慮され、最初の引数のみではないからです(メソッドとディスパッチの詳細については、メソッドを参照してください)。 したがって、関数が最初の引数だけに「属する」のは不適切です。 各オブジェクトの "内側"にたくさんの名前付きのメソッドをいれるより、メソッド群を編成して関数オブジェクトにする方が、言語設計上、非常に有益です。
複合型はstruct
キーワードに続けて、フィールド名のブロックをおき、必要に応じて::
を使って型注釈をつけて導入します。
julia> struct Foo
bar
baz::Int
qux::Float64
end
型注釈のないフィールドは、デフォルトのAny
型となり、従ってどんな型の値でも保持することができます。
型がFoo
の新しいオブジェクトは、型オブジェクトFoo
を、そのフィールド値に対して、関数を適用するようにして作成します。
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)
julia> typeof(foo)
Foo
型は関数のように適用する時には、 コンストラクタ と呼ばれます。 2つのコンストラクタが自動的に生成されます(これらは デフォルトコンストラクタ と呼ばれます)。 1つはどんな引数でも許容し、convert
を呼び出してフィールドの型に変換します。 もう1つはフィールドの型と完全に一致する引数だけを許容します。 2つのコンストラクタが生成されるのは、新しい定義を追加を簡単に、不注意でデフォルトのコンストラクタを置き換えることなく、 できるようにするためです。
bar
フィールドには型の制約はないので、値は何でも構いません。ただし、baz
の値はInt
に変換できる必要があります。
julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(Int64, 23.5)
Stacktrace:
[...]
fieldnames
関数を使うと、フィールド名のリストが表示されます。
julia> fieldnames(Foo)
(:bar, :baz, :qux)
従来のfoo.bar表記法を使用して、複合型のオブジェクトのフィールド値にアクセスできます。
julia> foo.bar
"Hello, world."
julia> foo.baz
23
julia> foo.qux
1.5
struct
で宣言された複合型オブジェクトは 不変 です。 生成後に変更することはできません。 これは最初は奇妙に思えるかもしれませんが、いくつかの利点があります:
より効率的になる場合があります。 構造体の中には効率的に配列にパックできるものもあり、 場合によっては、コンパイラが、不変オブジェクト全体を別のメモリに割り当てることを避けることができます。
型コンストラクタで規定される不変性を破ることはできません。
不変オブジェクトを使ったコードは、理解しやすくなる場合があります。
不変オブジェクトは、配列などの可変なオブジェクトをフィールドとして含んでも構いません。 含まれているオブジェクトは可変のままです。 不変オブジェクトのフィールド自体が、別のオブジェクトを参照するように変更できなくなるだけです。
必要に応じて、可変な複合オブジェクトをキーワードmutable struct
で宣言することができます (次のセクションで検討します)。
フィールドのない複合型はシングルトンです。そのような型のインスタンスは1つしか作れません。
julia> struct NoFields
end
julia> NoFields() === NoFields()
true
===
によって、NoFieldsの「2つの」生成されたインスタンスが、実際には同一であることを確認できます。 シングルトン型については、あとで で詳しく説明します。
複合型のインスタンスがどのように作成されるかについては、もっと多くの言うべきことがありますが、その議論はパラメータ型とメソッドの両方もかかわり、十分重要なので、独自の章コンストラクタで解説します。
可変複合型
struct
の代わりにmutable struct
で複合型を宣言すると、インスタンスは変更可能になります。
julia> mutable struct Bar
baz
qux::Float64
end
julia> bar = Bar("Hello", 1.5);
julia> bar.qux = 2.0
2.0
julia> bar.baz = 1//2
1//2
変更に対応できるように、このようなオブジェクトは、通常、ヒープ上に配置し、メモリアドレスは固定しています。 可変オブジェクトは、時間とともに値の変わりうる小さなコンテナと似ていて、アドレスだけで確実に識別できます。 対照的に、不変型のインスタンスは、特定のフィールド値に関連づけられています。 フィールド値だけで、オブジェクトに関するすべてがわかります。 型を可変にするかどうかを決めるには以下の問いを考えればいいでしょう。 同じフィールド値を持つ2つのインスタンスは同一だとみなせるか、あるいは時間とともに別々に変更する必要があるかと。 同一であると考えてもよいなら、おそらくその型は不変にすべきでしょう。
要約すると、2つの重要な特性がJuliaにおける普遍性を決定づけています。
- 不変型の値を変更することは、許可されていません。
- プリミティブ型では、一度設定された値のビットパターンは決して変わらず、その値はそのプリミティブ型で恒等的であることを意味します。
- 複合型では、そのフィールドの値の恒等性は決して変わらないことを意味します。 フィールドが、プリミティブ型の場合は、そのビットは決して変わらず、配列のような可変型の場合は、常に同じ可変の値を参照することを意味します。 可変な値の中身自体が変わった場合であってもです。
- 不変型のオブジェクトはコンパイラが自由にコピーすることができます。 というのも、不変性によって、元のオブジェクトとコピーしたものを見分けることができないからです。
- このため、特に十分小さな整数や浮動小数点数などの不変型は、通常レジスタ(やスタック)にある関数には そのまま渡されます。
- 一方、可変な値は、ヒープに配置され、その配置された値へのポインタとして関数に渡されます。 except in cases where the compiler is sure that there's no way to tell that this is not what is happening.
宣言型
上記のセクションで説明した3種の型(抽象型、プリミティブ型、複合型)は、実のところ、すべて密接に関連しています。 これらは重要な特徴が共通しています。
- 明示的に宣言している。
- 名前がある。
- スーパータイプを明示的に宣言している。
- パラメータをつけてもよい。
特徴が共通しているため、これらの型は内部的に同じ概念のDataType
のインスタンスとして表現されます。 DataType
はこれらの型のいずれかのことです。
julia> typeof(Real)
DataType
julia> typeof(Int)
DataType
DataType
は抽象型でも具象型でもかまいません。 具象型であれば、特定のサイズ、 格納領域の配置 があり、(場合によっては)フィールド名もあります。 そして、プリミティブ型は、サイズが0ではないDataType
で、フィールド名を持ちません。 複合型は、フィールド名があるか、空(サイズ0)のDataType
です。
システムのすべての具体的な値は、なんらかのDataType
のインスタンスです。
合併型
合併型は特殊な抽象型で、この型にオブジェクトとして含まれるのは、引数のいずれかの型のインスタンスすべてであり、 特殊なキーワードUnion
を使って構築します。
julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got Float64
多くの言語のコンパイラには、型推論のための内部でつかう合併構文があります。 Juliaは単にそれをプログラマにも公開しています。 型の数が少ない場合に 合併型
を使うと、Juliaのコンパイラは効率的なコードを生成します[1]。 なりうる型すべてに個別に特化したコードを生成します。
特に有益な合併型
はUnion{T, Nothing}
です。 ここでT
は任意の型、Nothing
は唯一のインスタンスがオブジェクトnothing
だけのシングルトン型です。 Juliaのこのパターンは、他の言語のNullable
,Option
,Maybe
型と同等です。 関数の引数やフィールドをUnion{T, Nothing}
として宣言すると、型T
の値か、値がないことを示すnothing
のどちらかに設定することができます。 詳細な情報はFAQのこの項目を参照してください。
パラメータ型
Juliaの型システムには、パラメータ化可能という重要かつ強力な特徴があります。 型にパラメータをつけると、型宣言は実質的に、とりうるパラメータ組み合わせに対応する、新しい型の種族全体を導入することになります。 多くの言語が汎化プログラミングに何らかの形で対応しています。 これは、必要な型を正確に指定しなくても、処理すべきデータ構造やアルゴリズムを特定することができます。 たとえば少し挙げるだけでも、ML、Haskell、Ada、Eiffel、C++、Java、C#、F#、およびScalaなどが、何らかの形で汎化プログラミングを取り入れています。 これらの言語の中には真のパラメータ多相(ML、Haskell、Scalaなど)に対応するものもあれば、テンプレートベースの汎化プログラミング(C ++、Javaなど)の形で対応するものもあります。 言語によって汎化プログラミングやパラメータ型は多種多様であるため、Juliaのパラメータ型を他の言語と比較することはせず、Julia自体のシステムについて説明することに専念します。 しかし、Juliaは動的型付け言語で、コンパイル時にすべての型を決定する必要はないため、静的パラメータ型付け言語の多くで生じる従来の困難が、比較的簡単に扱えることを注記しておきます。
すべての宣言型(DataType
の仲間)は、それぞれ同じ構文でパラメータ化できます。 まず、パラメータ複合型、次にパラメータ抽象型、最後にパラメータプリミティブ型という順番で説明します。
パラメータ複合型
型パラメータを導入するには、型名の直後に、中括弧で囲んで挿入します。
julia> struct Point{T}
x::T
y::T
end
この宣言では、型がT
の2つの「座標」を保持している新しいパラメータ型Point{T}
を定義しています。 T
とはなんだ、と誰かが尋ねるかもしれません。これがまさしくパラメータ型のポイントです。 どんな型(実際にはプリミティブ型の値でも構いませんが、ここでは明らかに型が使われています)でもかまいません。 Point{Float64}
は、Point
の定義で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
他の型は当然このサブタイプではありません。
julia> Float64 <: Point
false
julia> AbstractString <: Point
false
異なるT
の値がついた具象型Point
は決して互いにサブタイプとなることはありません。
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
この最後の点は 非常に 重要です。Float64 <: Real
は成り立つにもかかわらず、Point{Float64} <: Point{Real}
は 成り立ちません。
型理論の術語で言い換えると、Juliaの型パラメータは covariant (or even contravariant)ではなく、不変 です。 これは現実的の理由によります。 {Float64}
のインスタンスはPoint{Real}
のインスタンスと概念的には似ているかもしれませんが、2つ型のメモリ内の表現は異なります。
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
オブジェクトはどのように構成するのでしょうか。 [コンストラクタ](@ ref man-constructor)で詳しく説明しますが、複合型にに対して独自のコンストラクタを定義することは可能ですが、特別にコンストラクタの宣言をしなくても、デフォルトで新しい複合型オブジェクトを作成する方法が2通りあります。1つは型パラメータを明示的に与えるもの、もう1つはオブジェクトコンストラクタへの引数から暗黙裡に推定されるものです。
型Point{Float64}
は、T
の代わりにFloat64
を使って宣言したPoint
と同等の具象型なので、Point
と同じようなコンストラクタとしてそのまま適用できます。
julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
デフォルトのコンストラクタには、各フィールドに対してちょうど1つ引数を指定する必要があります。
julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
[...]
julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
[...]
パラメータ型に対しては、デフォルトのコンストラクタは1つしか生成されません。 オーバーライドできないためです。 このコンストラクタは任意の引数を受け取って、フィールドの型に変換します。
多くの場合、生成したいPoint
オブジェクトの型を指定するのは冗長です。 Point
コンストラクタを呼び出す際の引数に、すでに型情報が隠れているからです。 そのため、Point
のパラメータの型T
が推定可能で曖昧さがない場合は、Point
自体をコンストラクタとして適用することも可能です。
julia> Point(1.0,2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
julia> Point(1,2)
Point{Int64}(1, 2)
julia> typeof(ans)
Point{Int64}
Point
の場合、2つの引数が同じ型を持つ場合にのみ、型T
は明確に推定されます。 これ以外の場合、コンストラクタは失敗して、 MethodError
が発生します。
julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
Point(::T, !Matched::T) where T at none:2
このような型の混ざった場合でも適切に処理するコンストラクタメソッドは定義可能ですが、後述の[コンストラクタ](@ ref man-constructors)まで議論を保留します。
パラメータ抽象型
パラメータ抽象型に対しても、ほぼ同じ方法で、一群の抽象型に対して型宣言を行います。
julia> abstract type Pointy{T} end
この宣言では、T
は型や整数値を表し、Pointy{T}
は、それぞれのT
に対して別の抽象型になります。 パラメータ複合型と同様に、各インスタンスはPointy
のサブタイプです。
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
パラメータ抽象型は、パラメータ複合型と同じように不変です。
julia> Pointy{Float64} <: Pointy{Real}
false
julia> Pointy{Real} <: Pointy{Float64}
false
Juliaでは、Pointy{<:Real}
という表記で 共変型*** のようなもの、Pointy{>:Int}
で **反変型 のようなものを表現できます。 しかし、技術的には、これらは型の集合を表しています。(全合併型 参照)
julia> Pointy{Float64} <: Pointy{<:Real}
true
julia> Pointy{Real} <: Pointy{>:Int}
true
通常の抽象型は、具象型に対する有益な型の階層の作成に使いますが、パラメータ抽象型はパラメータ複合型と同じような目的で使います。 たとえば、Point{T}
をPointy{T}
のサブタイプとする宣言は次のようにできます。
julia> struct Point{T} <: Pointy{T}
x::T
y::T
end
この宣言で、それぞれの選んたT
に対して、Point{T}
はPointy{T}
のサブタイプとなります。
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{AbstractString} <: Pointy{AbstractString}
true
この関係も不変です。
julia> Point{Float64} <: Pointy{Real}
false
julia> Point{Float64} <: Pointy{<:Real}
true
Pointy
のようなパラメータ抽象型はなんの役に立つのでしょうか。 対角線 x = y 上にあるため、座標1つのみを必要とする点状のものを実装する場合を考えましょう。
julia> struct DiagPoint{T} <: Pointy{T}
x::T
end
ここでPoint{Float64}
とDiagPoint{Float64}
は共に、抽象型Pointy{Float64}
の実装で、これはT
に他のとりうる型を選んでも同じです。 これによりPoint
とDiagPoint
のどちらを実装するにも、Pointy
オブジェクトを共通のインタフェースにするようなプログラミングが可能になります。 しかし、完全な解説は、メソッドとディスパッチを導入する次の章メソッド に持ち越します。
型のパラメータのとりうる型を自由にしてしまうと、意味を成さない場合があります。 そのような状況では、次のように、T
の範囲を制限することができます。
julia> abstract type Pointy{T<:Real} end
この宣言では、T
が任意のReal
のサブタイプの場合で許容されますが、 Real
のサブタイプでなければ許容されません。
julia> Pointy{Float64}
Pointy{Float64}
julia> Pointy{Real}
Pointy{Real}
julia> Pointy{AbstractString}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Type{AbstractString}
julia> Pointy{1}
ERROR: TypeError: in Pointy, in T, expected T<:Real, got Int64
パラメータ複合型の型パラメータも、同じ方法で制限できます。
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
実世界でパラメータ型という仕組みがどれほど役立つかという例として、 ここでは整数の比を表すRational
という不変型を、Juliaで実際にどう定義するかを示します。 (単純化のため、ここではコンストラクタを省略します)
struct Rational{T<:Integer} <: Real
num::T
den::T
end
整数値の比率になる時だけ、意味をなすので、パラメータの型T
は、Integer
のサブタイプに限定されています。 整数の比は数直線上の値を表現するので、任意のRational
は、抽象型 Real
のインスタンスです。
タプル型
タプルとは関数本体からその引数だけを抜き出したものです。 関数の引数の目立った特徴は、順序と型です。 そのため、タプル型は、不変なパラメータ複合型で各パラメータがフィールドの型に対応しているものと似ています。 たとえば、2要素のタプル型は、次の複合型に似ています。
struct Tuple2{A,B}
a::A
b::B
end
ただし、3つの重要な違いがあります。
- タプル型は、任意の数のパラメータを持つことができます。
- タプル型は、そのパラメータと 共変 です。
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
直観的には、これは、関数の引数の型が関数のシグネチャのサブタイプであることに相当します(シグネチャが適合する場合)。
可変引数タプル型
タプル型の最後のパラメータは、特殊な型である可変引数
にすることが可能で、任意個数の後続の要素を表します。
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
は、第1要素はReal
のいずれかで、 第2要素は、各要素が第1要素の型を含む型の配列である配列の、2要素-タプルを参照します。
where
キーワード自体は、より複雑な宣言の内側でネストすることができます。 たとえば、次の宣言で作成される2つの型を考えてみましょう。
julia> const T1 = Array{Array{T,1} where T, 1}
Array{Array{T,1} where T,1}
julia> const T2 = Array{Array{T,1}, 1} where T
Array{Array{T,1},1} where T
型T1
は、1次元配列を要素とする、1次元配列を定義します。 内側の配列は同じ型のオブジェクトで構成されますが、この型は内側の配列ごとに異なる場合があります。 一方、型T2
は、すべての内側の配列の型が等しい1次元配列の1次元配列を定義します。 T2
は抽象型であり、Array{Array{Int,1},1} <: T2
であるのに対して、T1
は具象型である点に注意してください。 したがって、T1
は引数のないコンストラクタでa=T1()
のように構築することはできますが、T2
ではできません。
このような型を命名する便利な構文で、関数定義の短い形の構文と似ているものがあります:
Vector{T} = Array{T,1}
これはconst Vector = Array{T,1} where T
と同等です。 Vector{Float64}
と書くのは、Array{Float64,1}
と書くのと同等です。 包括型の Vector
がインスタンスとして持つのは、2番目のパラメータ(配列の次元数)が1である、要素の種類に関係ないすべてのArray
オブジェクトです。 パラメータ型を常に完全に指定しなければならない言語では、こういう構文はそんなに有用ではないかもしれません。 しかしJuliaでは、Vector
と書くだけで、任意の要素型のすべての1次元の密な配列を含む抽象型を表すことができます。
型エイリアス
すでに表現可能な型に新しい名前をつけると便利な場合が時々あります。 これは簡単な代入文で行うことができます。 たとえば、UInt
は、システム上のポインタのサイズによって、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
の特定のサイズの型の別名は存在しない点に注意してください。 整数レジスタとは異なり、浮動小数点レジスタのサイズは、IEEE-754規格で規定されています。 一方Int
のサイズは、そのマシン上のネイティブポインタのサイズを反映しています。)
型に対する演算
Juliaの型はそれ自体がオブジェクトなので、通常の関数を作用させることができます。 特に型の操作や探索に役立つ関数がいくつか既に導入されています。 <:
などは、左側の被演算子が右側の被演算子のサブタイプであるかどうかを示す演算子です。
isa
関数は、オブジェクトが指定された型であるかどうかを検査し、真か偽を返します。
julia> isa(1, Int)
true
julia> isa(1, AbstractFloat)
false
typeof()
関数は、既にこのマニュアルを通して例の中で使っていますが、引数の型を返します。 上述のように、型はオブジェクトであり、それ自体の型もあるので、型に対してその型が何であるかを尋ねることができます。
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,Float64,Rational})
DataType
julia> typeof(Union{Real,String})
Union
この操作を繰り返すとどうなるでしょうか?型の型の型は何でしょうか? 既にみたように、型はすべて複合型の値なので、すべてDataType
型になります。
julia> typeof(DataType)
DataType
julia> typeof(Union)
DataType
DataType
は自身の型でもあります。
ある種の型に対する別の演算には、supertype()
があり、型のスーパータイプを示します。 宣言型(DataType
)のみが明確なスーパータイプを持っています:
julia> supertype(Float64)
AbstractFloat
julia> supertype(Number)
Any
julia> supertype(AbstractString)
Any
julia> supertype(Any)
Any
supertype()
を他の型のオブジェクト(または、型ではないオブジェクト)に適用した場合は、MethodError
が発生します。
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
supertype(!Matched::DataType) at operators.jl:42
supertype(!Matched::UnionAll) at operators.jl:47
独自の整形表示
型のインスタンスがどのように表示されるかを独自に指定したい場合がよくあります。 これは、show()
関数のオーバーロードによって可能です。 たとえば、極座標形式で複素数を表す型を定義するとします。
julia> struct Polar{T<:Real} <: Number
r::T
Θ::T
end
julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar
この例では独自のコンストラクタ関数を追加して、異なるReal
型の引数をとると、それらを共通の型に昇格できるようにしました ( コンストラクタと変換と昇格を参照)。 (もちろん、Number
型と同じように動作させるためには、他にも多くのメソッドを定義する必要があるでしょう (例えば、+
、 *
、 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などの対話環境で単一のオブジェクトを表示する場合に、簡単な単一行形式は、オブジェクトを別の(配列などの)オブジェクトの一部として表示する場合にと、両方を行いたい場合があります。 デフォルトでは、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()
メソッドを定義 すべきではありません 。
さらに、show
メソッドを他のMIMEタイプ向けに定義して、対応している環境(例えばIJulia)では、オブジェクトをよりリッチな表示(HTML、画像など)にすることもできます。 たとえば、Polar
オブジェクトに対して書式付きのHTML表示を定義し、上付き文字と斜体を使うには、次のようにします。
julia> Base.show(io::IO, ::MIME"text/html", z::Polar{T}) where {T} =
println(io, "<code>Polar{$T}</code> complex number: ",
z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")
Polar
オブジェクトは、対応している環境ではHTMLを使用して自動的に表示されますが、必要なら手動でshow
を呼び出して、HTML出力することもできます。
julia> show(stdout, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>
An HTML renderer would display this as: Polar{Float64}
complex number: 3.0 e4.0 i
経験から言うと、単一行のshow
メソッドは、表示されたオブジェクトを生成する、Juliaとして有効な式を、表示すべきです。 このshow
メソッドが、中置演算子を含む時、たとえば、乗法の演算子(*
)が上述のPolar
の単一行のshow
メソッドに表れる時は、 別のオブジェクトの一部として印刷される場合は、正しく解析されないかもしれません。 この事例として、式オブジェクト(プログラムの表現を参照)でpolar
型の具体的なインスタンスの二乗を考えます。
julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
3.0 * exp(4.0im)
julia> print(:($a^2))
3.0 * exp(4.0im) ^ 2
^
は*
より優先順位が高いため、([演算子の優先順位と結合則(@ref)参照])、この式はa^2
を忠実に表現していません。 (3.0 * 1xp(4.0im)) ^ 2
になるはずです。 この問題を解決するために、独自のメソッドBase.show_unquoted(io::IO,z::Polar, indent::Int, precedence::Int)
を作ります。 これは表示の際に、内部的に式オブジェクトから呼びされます。
julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
if Base.operator_precedence(:*) <= precedence
print(io, "(")
show(io, z)
print(io, ")")
else
show(io, z)
end
end
julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)
上で定義されたメソッドは、呼び出す演算子の優先順位が乗法以上の時に、括弧を付け足します。 この検査によって、括弧なしでも、正しく解析される時は省略して表示できます。
julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)
julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)
場合によっては、文脈によってshow
メソッドの挙動を調整できると有益なことがあります。 これは、IOContext
型によって実現可能で、ラップしたIOストリームと一緒に文脈の特性の受け渡しができます。 例えば、:compact
プロパティをtrue
にすると、短い表現になり、false
や指定なしだと、かわりに長い表現になるというようにshow
メソッドを定義することができます。
julia> function Base.show(io::IO, z::Polar)
if get(io, :compact, false)
print(io, z.r, "ℯ", z.Θ, "im")
else
print(io, z.r, " * exp(", z.Θ, "im)")
end
end
この新しい簡潔な表現は、:compact
プロパティを持つIOContext
をIOストリームとして渡した時に利用可能です。 特に、配列を何段かで表示し、(水平方向の幅が制限されている)時に役立ちます。
julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im
julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Array{Polar{Float64},2}:
3.0ℯ4.0im 4.0ℯ5.3im
IOContext
の文書では、表示の調整に利用できる共通のプロパティのリストを参照できます。
"値型"
Juliaでは関数のディスパッチに、true
や false
のような 値 を利用できません。 しかし、パラメータ型によるディスパッチは可能で、その型パラメータとして「普通の」値(型、シンボル、整数、浮動小数点数、タプルなど)を使うことができます。 よくある例としては、Array{T,N}
で使われる次元のパラメータがあり、この場合T
は型( Float64
など)ですが、N
はただのInt
型の値です。
値をパラメータとする独自の型を作成して、その型によってディスパッチを制御することができます。 この考え方を説明するために、パラメータ型のVal{T}
を導入しましょう。 手の込んだ階層を必要としない時は、この手法にはこの型を慣用的に使います。
Val{T}
は次のように定義します。
julia> struct Val{x}
end
julia> Val(x) = Val{x}()
Val
Val
の実装は、これ以上はありません。 Juliaの標準ライブラリの関数にはVal
型のインスタンスを引数にとるものもあり、また独自の関数を書く時にも、Val
型は利用できます。例えば、
julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)
julia> firstlast(::Val{false}) = "Last"
firstlast (generic function with 2 methods)
julia> firstlast(Val(true))
"First"
julia> firstlast(Val(false))
"Last"
Juliaでは一貫性を保つために、呼び出し側は、常にVal
の 型 を使うのではなく、Val
の インスタンス を渡す必要があります。 つまり、foo(Val{:bar})
ではなくfoo(Val(:bar))
です。
Val
を含めて、パラメトリックな「値」型は非常に誤用しやすい点に注意してください。 ひどい時には、コードのパフォーマンスを簡単に大幅に 悪化 させることもありえます。 特に、上述のようなコードを実用のコードとして書きたいとは決して思わないでしょう。 適切な(そして不適切な)Val
の使い方の詳細については、the performance tipsの広範に渡る議論を読んでください。
"Small" は定数 MAX_UNION_SPLITTING
で定義されており、現在は4に設定されています。