型
型システムは、従来、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
このように型が混在するケースを適切に処理するコンストラクター メソッドは定義できますが、後で コンストラクタに議論は譲ります。
パラメトリック抽象型
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 types may have any number of parameters.
- Tuple types are covariant in their parameters:
Tuple{Int}
is a subtype ofTuple{Any}
. ThereforeTuple{Any}
is considered an abstract type, and tuple types are only concrete if their parameters are. - Tuples do not have field names; fields are only accessed by index.
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 Number
s that are able to contain Int
s (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 Val
instance 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.
ここでいう、"少数" はMAX_UNION_SPLITTING
定数で定義され、現在 4 に設定されています。