これまで、シラバスでは型についてあまり触れてきませんでしたが、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コンパイラーは(すべての)コードに目を通し、変数がどのように使われているかを見て、その使われ方に合った適切な既定の型を_推論_します。
簡単なチュートリアルの演習を解くだけなら、型推論に頼っても問題ありません。しかし、より大きなプログラムでは、もっと精密な制御が必要になることが多いでしょう。
最近のほとんどのプロセッサーでは、整数は既定で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型であり、これからもずっとInt16型のままです。
そのため、コンパイラーはyのために何バイト予約すればよいかを把握でき、残りのコードも安定した型を前提に最適化できます。
この点で、この変数はCのような静的型付け言語の変数とほぼ同じです。
Int64、Int16、String、Char。これらの型は、いったいどこから_「来る」_のでしょうか。
多くのオブジェクト指向(OO)言語では、各型はクラスであり、サブクラス化によってクラス階層に整理され、クラスメソッドが振る舞いを定義します。
JavaとRubyはこのパターンの明らかな例ですが、Pythonも内部的には似ています。
Juliaにはクラスがありません。
文書化された理由は、OOの機能がJITコンパイラーを妨げ、実行時の性能を損なうからです。
それでも、次のコードを見てください。
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
型の階層が木構造をなしていることを見ました(計算機科学の意味での木で、根が一番上にあります)。
木の各項目はnodeであり、これらはいくつかのカテゴリーに分けられます。
abstractと呼ばれます。concreteと呼ばれます。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を作成することに注意してください。
この演習では、成績データを前処理するという、ちょっとしたデータエンジニアリングに取り組みます。とてつもなく多く(多すぎる?)の生徒を抱える学校が、生徒が受け取った成績を分析しようとしていて、その作業を効率よく進めたいと考えています。そこで、データをできるだけ小さくまとめて整理するのが狙いです。
成績の尺度は通常0〜10なので、成績1つにつき8ビットしか使わないデータ型を使うのがもっとも効率的です。
UInt8データ型に変換する必要があります。Int8データ型に変換する必要があります。成績を変換する関数を実装できたら、次に、その成績を収めているコレクションを扱う関数を書きます。成績を降格し、降順に並べ替えて返すようにします。
Vectorで保存する人もいます。Setに成績を保存しています。どちらの関数でも、不正な入力はMethodErrorをスローして処理する必要があります。
例外処理は後のコンセプトで扱いますので、この演習では次の構文を使えば大丈夫です。
throw(MethodError(f, args))
ここでfは関数、argsはその関数に入力される引数のタプルです。
demote(n)関数の場合、fはdemote、argsは(n,)です。
throw(MethodError(demote, (n,)))
demote(n)メソッドを実装します。
Float64の入力に対しては、最も近い整数に切り上げ、UInt8データ型で返します。Integerに対しては、同じ整数をInt8データ型で返します。julia> demote(4.2)::UInt8
5
julia> demote(4)::Int8
4
julia> demote("hi")
MethodError: no method matching demote(::String) #output truncated
preprocess(coll)メソッドを実装します。
Vectorの入力に対しては、すべての数値を降格し、ベクターを逆順にします。Setの入力に対しては、すべての数値を降格し、降順に並べ替えたベクターを返します。julia> preprocess([1, 2, 3])
3-element Vector{Int8}:
3
2
1
julia> preprocess(Set([2.2, 5.8, 3.4]))
3-element Vector{UInt8}:
6
4
3
julia> preprocess(42)
MethodError: no method matching preprocess(::Int64) #output truncated