コンテンツへスキップ

型推論

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. リテラル値を代入する

リテラルがインスタンス変数に代入されると、リテラルの型がセットに追加されます。すべてのリテラルには、関連付けられた型があります。

次の例では、@nameStringと推論され、@ageInt32と推論されます。

class Person
  def initialize
    @name = "John Doe"
    @age = 0
  end
end

このルールとそれに続くすべてのルールは、initialize以外のメソッドにも適用されます。例えば

class SomeObject
  def lucky_number
    @lucky_number = 42
  end
end

上記の場合、@lucky_numberInt32 | Nilと推論されます。42が割り当てられたためInt32、クラスのinitializeメソッドのすべてで割り当てられなかったためNilです。

2. クラスメソッドnewの呼び出し結果を代入する

Type.new(...)のような式がインスタンス変数に代入されると、型Typeがセットに追加されます。

次の例では、@addressAddressと推論されます。

class Person
  def initialize
    @address = Address.new("somewhere")
  end
end

これはジェネリック型にも適用されます。ここで、@valuesArray(Int32)と推論されます。

class Something
  def initialize
    @values = Array(Int32).new
  end
end

: newメソッドは型によって再定義される場合があります。その場合、推論される型は、次のルールのいずれかを使用して推論できる場合は、newによって返される型になります。

3. 型制約のあるメソッドパラメータである変数を代入する

次の例では、メソッドパラメータnameString型の型制約があり、そのパラメータが@nameに代入されるため、@nameStringと推論されます。

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

上記の場合、コンパイラは@nameStringと推論し、後でそのメソッドを完全に型付けするときに、Int32String型の変数に代入できないというコンパイル時エラーが発生します。@nameStringでない場合は、明示的な型制約を使用してください。

4. 戻り値の型制約のあるクラスメソッドの結果を代入する

次の例では、クラスメソッドAddress.unknownの戻り値の型制約がAddressであるため、@addressAddressと推論されます。

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 関数は明示的な型を持つ必要があるため、コンパイラはインスタンス変数に代入するときに戻り値の型を使用できます。

次の例では、@ageInt32 であると推論されます。

class Person
  def initialize
    @age = LibPerson.compute_default_age
  end
end

lib LibPerson
  fun compute_default_age : Int32
end

7. out lib 式の使用

lib 関数は明示的な型を持つ必要があるため、コンパイラはポインタ型であるべき out 引数の型を使用し、逆参照された型を推測として使用できます。

次の例では、@ageInt32 であると推論されます。

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_numberInt32 | Nil であると推論されます。これは遅延初期化された変数に非常に役立ちます。

定数も、コンパイラ(と人間)にとっては非常に簡単なので、追跡されます。

class SomeObject
  DEFAULT_LUCKY_NUMBER = 42

  def initialize(@lucky_number = DEFAULT_LUCKY_NUMBER)
  end
end

ここではルール 5 (デフォルトのパラメータ値) が使用されており、定数が整数リテラルに解決されるため、@lucky_numberInt32 であると推論されます。