コンテンツへスキップ

型制約

型制約は、メソッドのパラメータに適用され、そのメソッドで受け入れる型を制限します。

def add(x : Number, y : Number)
  x + y
end

# Ok
add 1, 2

# Error: no overload matches 'add' with types Bool, Bool
add true, false

型制約なしで`add`を定義した場合も、コンパイル時エラーが発生することに注意してください。

def add(x, y)
  x + y
end

add true, false

上記のコードは、このコンパイルエラーを生成します。

Error in foo.cr:6: instantiating 'add(Bool, Bool)'

add true, false
^~~

in foo.cr:2: undefined method '+' for Bool

  x + y
    ^

これは、`add`を呼び出すと、引数の型を使用してインスタンス化されるためです。異なる型の組み合わせでメソッドを呼び出すたびに、異なるメソッドのインスタンスが生成されます。

唯一の違いは、最初のエラーメッセージの方が少し分かりやすいことです。しかし、どちらの定義も、コンパイル時エラーが発生するため安全です。そのため、一般的には、型制約を指定せず、ほとんどの場合、異なるメソッドのオーバーロードを定義するためにのみ使用するのが好ましいです。これにより、より汎用的で再利用可能なコードになります。たとえば、`+`メソッドを持つが`Number`ではないクラスを定義した場合、型制約のない`add`メソッドを使用できますが、制約のある`add`メソッドは使用できません。

# A class that has a + method but isn't a Number
class Six
  def +(other)
    6 + other
  end
end

# add method without type restrictions
def add(x, y)
  x + y
end

# OK
add Six.new, 10

# add method with type restrictions
def restricted_add(x : Number, y : Number)
  x + y
end

# Error: no overload matches 'restricted_add' with types Six, Int32
restricted_add Six.new, 10

型文法を参照して、型制約で使用される表記を確認してください。

型制約は、実際のメソッド内の変数には適用されないことに注意してください。

def handle_path(path : String)
  path = Path.new(path) # *path* is now of the type Path
  # Do something with *path*
end

インスタンス変数からの制約

場合によっては、メソッドのパラメータの型を、その使用方法に基づいて制限することができます。たとえば、次の例を考えてみてください。

class Foo
  @x : Int64

  def initialize(x)
    @x = x
  end
end

この場合、初期化関数の`x`パラメータは`Int64`でなければならないことが分かっています。それを無制限のままにする理由は何もありません。

コンパイラは、メソッドのパラメータからインスタンス変数への代入を見つけると、そのような制約を挿入します。上記の例では、`Foo.new "hi"`を呼び出すと、(型制約に注意してください)失敗します。

Error: no overload matches 'Foo.new' with type String

Overloads are:
 - Foo.new(x : ::Int64)

self 制約

特別な型制約は`self`です。

class Person
  def ==(other : self)
    other.name == name
  end

  def ==(other)
    false
  end
end

john = Person.new "John"
another_john = Person.new "John"
peter = Person.new "Peter"

john == another_john # => true
john == peter        # => false (names differ)
john == 1            # => false (because 1 is not a Person)

前の例では、`self`は`Person`と書くことと同じです。しかし、一般的に、`self`は最終的にそのメソッドを所有することになる型を書くことと同じであり、モジュールが関与する場合、より有用になります。

`Person`は`Reference`を継承しているので、`==`の2番目の定義は、`Reference`で既に定義されているため、必要ありません。

`self`は、クラスメソッドでも、常にインスタンスタイプに対する一致を表すことに注意してください。

class Person
  getter name : String

  def initialize(@name)
  end

  def self.compare(p1 : self, p2 : self)
    p1.name == p2.name
  end
end

john = Person.new "John"
peter = Person.new "Peter"

Person.compare(john, peter) # OK

`Person`型に制限するには`self.class`を使用できます。次のセクションでは、型制約における`.class`サフィックスについて説明します。

クラスとしての制約

たとえば、`Int32`を型制約として使用すると、メソッドは`Int32`のインスタンスのみを受け入れるようになります。

def foo(x : Int32)
end

foo 1       # OK
foo "hello" # Error

`Int32`型(そのインスタンスではなく)のみを受け入れるメソッドが必要な場合は、`.class`を使用します。

def foo(x : Int32.class)
end

foo Int32  # OK
foo String # Error

上記は、インスタンスではなく型に基づいてオーバーロードを提供する場合に役立ちます。

def foo(x : Int32.class)
  puts "Got Int32"
end

def foo(x : String.class)
  puts "Got String"
end

foo Int32  # prints "Got Int32"
foo String # prints "Got String"

スプラットにおける型制約

スプラットに型制約を指定できます。

def foo(*args : Int32)
end

def foo(*args : String)
end

foo 1, 2, 3       # OK, invokes first overload
foo "a", "b", "c" # OK, invokes second overload
foo 1, 2, "hello" # Error
foo()             # Error

型を指定する場合、タプル内のすべての要素はその型と一致する必要があります。さらに、空のタプルは上記のいずれの場合にも一致しません。空のタプルに対応する必要がある場合は、別のオーバーロードを追加します。

def foo
  # This is the empty-tuple case
end

任意の型の1つ以上の要素に一致する簡単な方法は、制約として`_`を使用することです。

def foo(*args : _)
end

foo()       # Error
foo(1)      # OK
foo(1, "x") # OK

自由変数

`forall`を使用して、型制約に引数の型、または引数の型の部分を代入させることができます。

def foo(x : T) forall T
  T
end

foo(1)       # => Int32
foo("hello") # => String

つまり、`T`はメソッドのインスタンス化に使用された型になります。

自由変数は、型制約内でジェネリック型の型引数を抽出するために使用できます。

def foo(x : Array(T)) forall T
  T
end

foo([1, 2])   # => Int32
foo([1, "a"]) # => (Int32 | String)

型のインスタンスではなく型名を受け入れるメソッドを作成するには、型制約内の自由変数に`.class`を追加します。

def foo(x : T.class) forall T
  Array(T)
end

foo(Int32)  # => Array(Int32)
foo(String) # => Array(String)

複数の引数の型を一致させるために、複数の自由変数を指定することもできます。

def push(element : T, array : Array(T)) forall T
  array << element
end

push(4, [1, 2, 3])      # OK
push("oops", [1, 2, 3]) # Error

スプラット型制約

スプラットパラメータの制約にもスプラットが含まれる場合、その制約はTuple型を指定する必要があり、パラメータに対応する引数は、スプラット制約の要素と一致する必要があります。

def foo(*x : *{Int32, String})
end

foo(1, "") # OK
foo("", 1) # Error
foo(1)     # Error

上記は単にスプラットを使用しないことで表現できるため(つまり、`def foo(x : Int32, y : String)`)、スプラット制約にタプル型を直接指定することは非常にまれです。しかし、制約が自由変数の場合は、対応するすべての引数の型を含む`Tuple`であると推論されます。

def foo(*x : *T) forall T
  T
end

foo(1, 2)  # => Tuple(Int32, Int32)
foo(1, "") # => Tuple(Int32, String)
foo(1)     # => Tuple(Int32)
foo()      # => Tuple()

最後の行では、`T`は空のタプルであると推論されますが、これはスプラット以外の制約を持つスプラットパラメータでは不可能です。

ダブルスプラットパラメータも同様に、ダブルスプラット型制約をサポートします。

def foo(**x : **T) forall T
  T
end

foo(x: 1, y: 2)  # => NamedTuple(x: Int32, y: Int32)
foo(x: 1, y: "") # => NamedTuple(x: Int32, y: String)
foo(x: 1)        # => NamedTuple(x: Int32)
foo()            # => NamedTuple()

さらに、複数の型引数を一度に抽出するために、ジェネリック型内にもシングルスプラット制約を使用できます。

def foo(x : Proc(*T, Int32)) forall T
  T
end

foo(->(x : Int32, y : Int32) { x + y }) # => Tuple(Int32, Int32)
foo(->(x : Bool) { x ? 1 : 0 })         # => Tuple(Bool)
foo(->{ 1 })                            # => Tuple()