コンテンツにスキップ

型制約

型制約はメソッドの引数に適用されて、受け取ることができる型を制約するものです。

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 メソッドが型制約を持っている場合はそれができません。

# + メソッドを持っているけれど Number ではないクラス
class Six
  def +(other)
    6 + other
  end
end

# 型制約を持たない add メソッド
def add(x, y)
  x + y
end

# OK
add Six.new, 10

# 型制約を持つ add メソッド
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* はこれによって Path 型になった
  # *path* を使う
end

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 (名前が異なるため)
john == 1            # => false (1 は Person 型ではないため)

上記の例では self を指定するのは、そのまま Person と書くことと同じです。しかし、self と書くことは最終的にそのメソッドが呼び出される際の自身の型となるので、モジュールのメソッドなどの場合に便利です。

また、これは補足ですが、PersonReference を継承しているため、実際には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  # "Got Int32" と表示
foo String # "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

このように型を指定した場合、タプルのすべての要素がその型である必要があります。また、空のタプルは上記の例ではマッチしません。もし空のタプルもサポートしたいのであれば、もう1つオーバーロードを追加してください。

def foo
  # タプルが空の場合
end

どんな型の1つ以上の引数に対してもマッチするようにしたい場合、Object を型制約に指定することもできます。

def foo(*args : Object)
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