ブロックとは、メソッドに渡すことができるコードのまとまりです。
他のプログラミング言語では、これは無名関数やラムダと呼ばれます。
メソッドは、ブロックをコードのまとまりを表すProcに変換できます。
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
ブロックとProcは、繰り返し処理やコールバックなど、Crystalで非常によく使われます。
また、DSL(ドメイン固有言語)を作るのにも使われます。DSLの例としては、WebフレームワークのKemalや、テストフレームワークのSpec(Crystalの解答をテストするのに使われるものです)があります。
ブロックを受け取るメソッドを作るには、最後の引数の前にアンパサンド(&)を付けます。
これにより、ブロックがProcに変換され、その引数に代入されます。
あとはyieldを使うと、メソッドに渡されたブロックが実行されます。
def my_method(&block)
1 + yield
end
ブロックを渡す方法は2つあり、do ... endキーワードを使うか、波括弧({})を使うかのどちらかです。
主な違いは優先順位です。do ... endは複数行のブロックを書けますが、波括弧({})は書けません。
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yieldyieldは、メソッドに渡されたブロックを実行するキーワードです。
つまり、yieldを呼び出さなければ、ブロックが実行されることはありません。
また、yieldには引数を渡すこともでき、その引数はブロックに渡されます。
ブロックに渡せる引数の数に制限はありません。
ブロックの中で引数を使うには、|x, y, ...|という構文を使います。
ここで、x、y、...は引数の名前です。
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
上の例では、ブロックに引数2が渡され、ブロックはそれを3倍して6を返します。
そしてメソッドは、その結果に1を足して7を返します。
yieldに渡された引数の数が、ブロックが受け取る引数の数より多いと、エラーが発生します。
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
ブロックは、明示的に書くことも暗黙的に書くこともできます。 暗黙的なブロックが最も一般的で、上の例で示したのもこれです。 ブロックにさまざまな型の引数を渡すこともでき、その場合コンパイラーが型を推論してユニオン型を作ります。
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
明示的なブロックを書くときは、引数の型と戻り値の型を指定できます。
これはArg1Type, Arg2Type, ... -> ResultTypeという構文で行います。
ここでは、引数はコンマ(,)で区切り、戻り値の型は->で区切ります。
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
他の明示的な型と同様に、コンパイラーは型が期待どおりであることを強制します。 ブロックがメソッドに指定された型と一致しない場合は、エラーが発生します。
引数を1つだけ取り、その引数に対してメソッドを1つだけ呼び出すブロックには、省略記法があります。
|x|を使う代わりに、引数名の前に&を付けられます。
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
ProcProcは、ブロックと非常に深い関係があります。
決定的な違いは、Procは型ですが、ブロックは型ではないという点です。
Procは関数ポインターを表し、変数として受け渡しできます。
Procを定義するには、procリテラルの->(args) { ... }という構文を使います。
引数はコンマ(,)で区切り、ブロックは{ ... }で区切ります。
Procを呼び出すには、callメソッドを使い、引数を渡します。
引数の型は必須です。
戻り値の型はprocの本体から推論されますが、明示的に指定することもできます。
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
ブロックのキャプチャは、ブロックからProcを作る方法です。
メソッドの最後の引数の前に&演算子を付けることで行います。
これにより、ブロックがProcに変換され、その引数に代入されます。
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Procを渡すProcは、ブロックを期待するメソッドに渡すことができます。
条件によって渡すブロックを変えたいときに便利です。
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
変数を宣言するときは、: Typeという構文でその変数の型を宣言できます。
Procの場合は、Proc((ArgType, ...), ResultType)という構文を使います。
最後の型は常に戻り値の型で、それ以外は引数の型です。
つまり、型が1つだけの場合はそれが戻り値の型となり、そのProcは引数を取らないことになります。
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
Procを宣言するメソッドからProcを宣言することもできます。
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)