型推論¶
Crystalの理念は、できるだけ少ない型制約を要求することです。しかし、いくつかの制約は必要です。
このようなクラス定義を考えてみましょう。
class Person
def initialize(@name)
@age = 0
end
end
@ageは整数であることがすぐにわかりますが、@nameの型はわかりません。コンパイラは、Personクラスのすべての使用箇所からその型を推論できます。しかし、そうするといくつかの問題が発生します。
- 型は、コードを読む人間にとっては明らかではありません。彼らは、これを知るために
Personのすべての使用箇所を確認する必要があります。 - メソッドを一度だけ分析する必要がある、インクリメンタルコンパイルなどのコンパイラの最適化は、ほとんど不可能になります。
コードベースが成長するにつれて、これらの問題はより重要になります。プロジェクトを理解するのが難しくなり、コンパイル時間が耐えられなくなります。
このため、Crystalは、インスタンス変数とクラス変数の型を、(人間にとって明らかなように) 明確な方法で知る必要があります。
これをCrystalに知らせるには、いくつかの方法があります。
型制約付き¶
最も簡単な方法ですが、おそらく最も面倒な方法は、明示的な型制約を使用することです。
class Person
@name : String
@age : Int32
def initialize(@name)
@age = 0
end
end
型制約なし¶
明示的な型制約を省略した場合、コンパイラは、いくつかの構文規則を使用して、インスタンス変数とクラス変数の型を推論しようとします。
特定のインスタンス/クラス変数について、ルールが適用され、型を推測できる場合、その型はセットに追加されます。それ以上ルールを適用できなくなると、推論された型はそれらの型のユニオンになります。さらに、コンパイラがインスタンス変数が常に初期化されるとは限らないと推論した場合、Nil型も含まれます。
ルールはたくさんありますが、通常、最初の3つが最も使用されます。それらをすべて覚えておく必要はありません。コンパイラがインスタンス変数の型を推論できないというエラーを出す場合は、いつでも明示的な型制約を追加できます。
次のルールはインスタンス変数のみに言及していますが、クラス変数にも適用されます。それらは次のとおりです。
1. リテラル値を代入する¶
リテラルがインスタンス変数に代入されると、リテラルの型がセットに追加されます。すべてのリテラルには、関連付けられた型があります。
次の例では、@nameはStringと推論され、@ageはInt32と推論されます。
class Person
def initialize
@name = "John Doe"
@age = 0
end
end
このルールとそれに続くすべてのルールは、initialize以外のメソッドにも適用されます。例えば
class SomeObject
def lucky_number
@lucky_number = 42
end
end
上記の場合、@lucky_numberはInt32 | Nilと推論されます。42が割り当てられたためInt32、クラスのinitializeメソッドのすべてで割り当てられなかったためNilです。
2. クラスメソッドnewの呼び出し結果を代入する¶
Type.new(...)のような式がインスタンス変数に代入されると、型Typeがセットに追加されます。
次の例では、@addressはAddressと推論されます。
class Person
def initialize
@address = Address.new("somewhere")
end
end
これはジェネリック型にも適用されます。ここで、@valuesはArray(Int32)と推論されます。
class Something
def initialize
@values = Array(Int32).new
end
end
注: newメソッドは型によって再定義される場合があります。その場合、推論される型は、次のルールのいずれかを使用して推論できる場合は、newによって返される型になります。
3. 型制約のあるメソッドパラメータである変数を代入する¶
次の例では、メソッドパラメータnameにString型の型制約があり、そのパラメータが@nameに代入されるため、@nameはStringと推論されます。
class Person
def initialize(name : String)
@name = name
end
end
メソッドパラメータの名前は重要ではないことに注意してください。これも同様に機能します
class Person
def initialize(obj : String)
@name = obj
end
end
メソッドパラメータからインスタンス変数を代入するためのより短い構文を使用しても、同じ効果があります
class Person
def initialize(@name : String)
end
end
また、コンパイラは、メソッドパラメータに別の値が再代入されるかどうかをチェックしないことに注意してください。
class Person
def initialize(name : String)
name = 1
@name = name
end
end
上記の場合、コンパイラは@nameをStringと推論し、後でそのメソッドを完全に型付けするときに、Int32をString型の変数に代入できないというコンパイル時エラーが発生します。@nameがStringでない場合は、明示的な型制約を使用してください。
4. 戻り値の型制約のあるクラスメソッドの結果を代入する¶
次の例では、クラスメソッドAddress.unknownの戻り値の型制約がAddressであるため、@addressはAddressと推論されます。
class Person
def initialize
@address = Address.unknown
end
end
class Address
def self.unknown : Address
new("unknown")
end
def initialize(@name : String)
end
end
実際、上記のコードでは、self.unknownに戻り値の型制約は必要ありません。その理由は、コンパイラがクラスメソッドの本体も調べ、前のルールのいずれかを適用できる場合 (それはnewメソッドであるか、リテラルであるなど)、その式から型を推論するためです。したがって、上記は次のように簡単に記述できます
class Person
def initialize
@address = Address.unknown
end
end
class Address
# No need for a return type restriction here
def self.unknown
new("unknown")
end
def initialize(@name : String)
end
end
この追加ルールは、newに加えて「コンストラクタのような」クラスメソッドを持つことが非常に一般的であるため、非常に便利です。
5. デフォルト値を持つメソッドパラメータの変数への代入¶
次の例では、name のデフォルト値が文字列リテラルであり、後で @name に代入されるため、推論される型のセットに String が追加されます。
class Person
def initialize(name = "John Doe")
@name = name
end
end
これはもちろん、短い構文でも機能します。
class Person
def initialize(@name = "John Doe")
end
end
デフォルトのパラメータ値は、Type.new(...) メソッドまたは戻り値の型の制限があるクラスメソッドにすることもできます。
6. lib 関数の呼び出し結果の代入¶
lib 関数は明示的な型を持つ必要があるため、コンパイラはインスタンス変数に代入するときに戻り値の型を使用できます。
次の例では、@age は Int32 であると推論されます。
class Person
def initialize
@age = LibPerson.compute_default_age
end
end
lib LibPerson
fun compute_default_age : Int32
end
7. out lib 式の使用¶
lib 関数は明示的な型を持つ必要があるため、コンパイラはポインタ型であるべき out 引数の型を使用し、逆参照された型を推測として使用できます。
次の例では、@age は Int32 であると推論されます。
class Person
def initialize
LibPerson.compute_default_age(out @age)
end
end
lib LibPerson
fun compute_default_age(age_ptr : Int32*)
end
その他のルール¶
コンパイラは、明示的な型制限を少なくするために、可能な限り賢く動作しようとします。たとえば、if 式を代入する場合、型は then ブランチと else ブランチから推論されます。
class Person
def initialize
@age = some_condition ? 1 : 2
end
end
上記の if (厳密には三項演算子ですが、if に似ています) は整数リテラルを持っているため、@age は冗長な型制限を必要とせずに、Int32 であると正しく推論されます。
もう一つのケースは || と ||= です。
class SomeObject
def lucky_number
@lucky_number ||= 42
end
end
上記の例では、@lucky_number は Int32 | Nil であると推論されます。これは遅延初期化された変数に非常に役立ちます。
定数も、コンパイラ(と人間)にとっては非常に簡単なので、追跡されます。
class SomeObject
DEFAULT_LUCKY_NUMBER = 42
def initialize(@lucky_number = DEFAULT_LUCKY_NUMBER)
end
end
ここではルール 5 (デフォルトのパラメータ値) が使用されており、定数が整数リテラルに解決されるため、@lucky_number は Int32 であると推論されます。