これまでのところ、シラバスは型についてあまり触れてきませんでしたが、Juliaに型が存在することは明らかです。
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
型を指定したわけではありませんが、Juliaが自動的に割り当ててくれました。
typesがあり、設計の中心となっています。Type Inferenceを使って型を「推測」できます。JITコンパイラーはコード全体に目を通し、変数がどのように使われているかを見て、その使われ方に合った適切なデフォルトの型を_推論_します。
これまでのコンセプトで、最も単純な算術をはじめ、数値の型を統一する例をたくさん見てきました。
julia> 2 + 1.3
3.3
整数(Int64)と浮動小数点数(Float64)を足すと、結果はFloat64になりました。同様に、
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
上の例では、タプルは各要素の型を保っていましたが、ベクトルに変換するとすべてが均一なFloat64に変わりました。
Juliaのコンパイラーは、どの型変換が可能かを理解しています。整数から小数へは問題ありませんが、小数から整数へは精度が失われるため、InexactErrorが発生します。型の昇格は、式の中のすべての値を共通の型に変換します。その型は、すべての入力と互換性を持てるだけの汎用性を備えています。
同じ変換は、promote()関数を使って明示的に行うこともできます。
julia> promote(nums...)
(3.0, 4.1, 0.25)
型推論と型の昇格に頼るのは、簡単なチュートリアルの演習を解くには十分ですが、より大きなプログラムでは、より正確な制御が必要になるでしょう。
ほとんどの現代のプロセッサーでは、整数はデフォルトでInt64になります。
Numbersのコンセプトで、値をデフォルト以外の特定の型に変換できることを見ました。
julia> x = Int16(42)
42
julia> typeof(x)
Int16
しかし、変数xには、別の型の値を再代入できます。
julia> x = "changed"
"changed"
julia> typeof(x)
String
これはtype instabilityです。つまり、
代わりに、::演算子を使ってxの型を設定できます。
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
これでyはInt16型になり、今後もずっとそのままです。そうなると、コンパイラーはyのために何バイト確保すればよいかを把握でき、コードの残りの部分を、安定した型を前提に最適化できます。この点で、この変数はCのような静的型付け言語の変数とほぼ同じです。
上で説明した型の割り当ては、変数への代入の左辺で使い、その変数の型を制約します。
値、または値に評価されるものに::演算子を使うのは、一般に、その値がこの型でなければならず、そうでなければエラーを投げるべきだという_アサーション_です。
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
よくあるのは、関数の戻り値に対して使い、関数が期待どおりに動いたかを確認する最後の簡単なチェックとする使い方です。
なお、他の形式のアサーションには@assertマクロがあります。
Int64、Int16、String、Char。これらの型は、どこから_"来た"_のでしょうか。
多くのオブジェクト指向(OO)言語では、各型はクラスであり、サブクラス化によってクラス階層が形づくられ、クラスメソッドが振る舞いを定義します。
JavaとRubyはこのパターンの明らかな例ですが、Pythonも内部的には似ています。
Juliaにはクラスがありません。
文書化されている理由は、OOの機能がJITコンパイラーを妨げ、実行時の性能を損なうからです。
もしかすると、彼らもこの言葉を読んだのかもしれません。
「オブジェクト指向プログラミングは、カリフォルニア以外では生まれようのない、極めて悪いアイデアだ。」
これはEdsger Dijkstraの言葉とされています。1950年代から数十年にわたって活躍した偉大な計算機科学者です(ただし、明朗な楽観主義や繊細な外交手腕で知られていたわけではありませんが)。
それでも、このコードを見てください。
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
いくつか補足すると、
Int16は型であり、この型の変数を作ることができます。Int16はSignedのサブタイプであり、supertype()関数でそれを確認できます。Integer、Real、Numberを経て頂点のAnyへと続いています。subtypes()を使うと、この階層の枝を一覧できます。すべての枝はAnyで終わります。Anyは、自分自身のスーパータイプであるという点で特別です。
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
つまり、Juliaにはclassの階層はありませんが、typeの階層は_あります_。
階層全体を示そうとすると巨大な木になり、実際に見ることは不可能です。
その一部を眺めることについては、オンラインで議論されています。
いずれ、これがどう動くのかを解きほぐしていきますが、その前に探求すべきことはもっとたくさんあります。
typeof()を使って、いつもの方法で等しいかどうかをテストできます。
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
型の等しさは完全に一致している必要があります。この形式の比較は、型の階層を理解しないからです。
より柔軟に、isaは、値が比較対象と同じ型か、そのサブタイプかどうかを教えてくれます。
中置記法でも関数形式でも使えます。
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
isaは左側にvalueを期待し、typeではないことに注意してください。
この方法で2つの_型_を比較しようとすると、予期しない結果になります。
正しい演算子は<:で、これから先のコンセプトでもっとたくさん登場します。
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
型のテストは、関数内の制御フローに使うこともできますが、慣用的なJuliaでは比較的珍しいです。
多重ディスパッチのコンセプトで見ますが、関数の引数に型を付け、そのようなロジックはJuliaのディスパッチ機構に任せるほうが効率的なことがよくあります。 ただし、多重ディスパッチにたどり着く前に、型に関連するさらに多くのコンセプトを議論する必要があります。
型の階層は木構造を形づくっているのを見ました(計算機科学の意味での木で、根が上にあります)。
木の各項目はnodeであり、いくつかのカテゴリーに分けられます。
julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
これは重要な区別です。変数としてinstantiatedできるのは具象型だけだからです。
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
抽象型を使おうとしても(この場合は)エラーメッセージは出ませんが、コンパイラーが適切な具象型を作ります。IntegerではなくInt64になります。少なくとも、変数に整数以外の値が代入されるのは防げます。
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
したがって、抽象型を使った型の割り当ては、変数の型に対する_制約_であり、(上の例では)Integerの任意のサブタイプに限定します。
これはプログラミング言語の世界では珍しいものです。Cの型宣言より弱く、最近のバージョンのPythonの型ヒントより強いのです。
isabstracttype()とisconcretetype()という関数でテストできます。
これらは単にお互いの否定ではないことに注意してください。後のコンセプトで見ますが、抽象でも具象でもない型もあります。
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
Vectorは具象型ではありませんが、その要素は具象です。
eltype()(要素の型)関数でこの型を取り出せます。
julia> eltype([1, 2.3])
Float64