ブロックと Proc¶
メソッドはコードのブロックを取ることができ、そのブロックは yield
キーワードによって実行できます。例をあげます。
def twice
yield
yield
end
twice do
puts "Hello!"
end
上記のプログラムでは各 yield
ごとに、計2回の "Hello!" が出力されます。
ブロックを受け取るメソッドを定義するには、単純に yield
をメソッド内で使います。そうするとコンパイラはそれをブロックを受け取るメソッドであると認識します。ダミーのブロック引数を宣言することで、上記をより明確に示すこともできます。先頭がアンパサンド (&
) の引数を最後の引数にしてください。
def twice(&block)
yield
yield
end
ブロックを渡してメソッドを呼び出すには、do ... end
か { ... }
を利用します。以下はすべて同等のコードです。
twice() do
puts "Hello!"
end
twice do
puts "Hello!"
end
twice { puts "Hello!" }
do ... end
と { ... }
の違いは、do ... end
は最も左の呼び出しに渡されますが、{ ... }
は最も右にある呼び出しに渡されるということです。
foo bar do
something
end
# 上記は以下に同じ
foo(bar) do
something
end
foo bar { something }
# 上記は以下に同じ
foo(bar { something })
このようになっている理由はドメイン固有言語 (DSL) を作成する際に、do ... end
が自然な英語のように読み下せるようにするためです。
open file "foo.cr" do
something
end
# Same as:
open(file("foo.cr")) do
something
end
上記を次のようにすることはできません。
open(file("foo.cr") do
something
end)
オーバーロード¶
オーバーロードで説明したように、ブロックを取るメソッドとそうでないメソッドは別のオーバーロードであると解釈されます。
yield の引数¶
yield
式はメソッド呼び出しと似ていて、引数を受け取ることもできます。例をあげます。
def twice
yield 1
yield 2
end
twice do |i|
puts "Got #{i}"
end
上記を実行すると "Got 1" そして "Got 2" と出力されます。
波カッコを使った指定も可能です。
twice { |i| puts "Got #{i}" }
複数の値を yield
することもできます。
def many
yield 1, 2, 3
end
many do |x, y, z|
puts x + y + z
end
# Output: 6
ブロックに指定するのが yield される引数の数より少なくても構いません。
def many
yield 1, 2, 3
end
many do |x, y|
puts x + y
end
# Output: 3
しかし yield された引数の数よりも多くのブロック引数を指定した場合はエラーとなります。
def twice
yield
yield
end
twice do |i| # Error: too many block parameters
end
ブロックの引数はすべての yield 式に応じた型を持ちます。例をあげます。
def some
yield 1, 'a'
yield true, "hello"
yield 2, nil
end
some do |first, second|
# first は Int32 | Bool
# second は Char | String | Nil
end
ブロック引数にアンダースコアも使えます。
def pairs
yield 1, 2
yield 2, 4
yield 3, 6
end
pairs do |_, second|
print second
end
# Output: 246
単一引数の場合の短縮記法¶
ブロックが単一の引数とそれに対するメソッド呼び出しのみである場合、ブロックはより短い記法で置き換えれます。
例えば、
method do |param|
param.some_method
end
and
method { |param| param.some_method }
は次のように書けます。
method &.some_method
あるいはこのようにも、
method(&.some_method)
両方の場合で、&.some_method
は method
に引数として渡されています。この引数は意味的にはブロックと同様です。これは単なるシンタックスシュガーであり、パフォーマンス上の欠点はありません。
メソッドが他の引数を要求する場合、短縮句法はメソッドの引数リストに追加されなければいけません。
["a", "b"].join(",", &.upcase)
これは次に等しいです。
["a", "b"].join(",") { |s| s.upcase }
短縮記法に引数を渡すこともできます。
["i", "o"].join(",", &.upcase(Unicode::CaseOptions::Turkic))
演算子の場合も同様に呼び出すことができます。
method &.+(2)
method(&.[index])
yield の値¶
yield
式自体も値を持っていて、それはブロックの最後の値となります。例をあげます。
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
i + 1
end
上記では "2" と "3" が出力されます。
yield
式の値は、主に値の変換やフィルタリングの際に有効に利用できます。その最もわかりやすい例は Enumerable#map と Enumerable#select でしょう。
ary = [1, 2, 3]
ary.map { |x| x + 1 } # => [2, 3, 4]
ary.select { |x| x % 2 == 1 } # => [1, 3]
1つ簡単な変換メソッドを例にあげます。
def transform(value)
yield value
end
transform(1) { |x| x + 1 } # => 2
この最後の式の実行結果は 2
になります。transform
メソッドの最後の式は yield
であり、そしてその値はブロックの最後の式になるからです。
型制約¶
yield
で使うブロックの型を &block
によって制約することができます。例をあげます。
def transform_int(start : Int32, &block : Int32 -> Int32)
result = yield start
result * 2
end
transform_int(3) { |x| x + 2 } # => 10
transform_int(3) { |x| "foo" } # Error: expected block to return Int32, not String
break¶
ブロックの中に break
式があると、そこでメソッドを抜けます。
def thrice
puts "Before 1"
yield 1
puts "Before 2"
yield 2
puts "Before 3"
yield 3
puts "After 3"
end
thrice do |i|
if i == 2
break
end
end
上記は "Before 1" そして "Before 2" を出力します。break
があるため、thrice
メソッドが puts "Before 3"
を実行することはありません。
break
は引数を受け取ることも可能で、その場合にはそれがメソッドの戻り値となります。例をあげます。
def twice
yield 1
yield 2
end
twice { |i| i + 1 } # => 3
twice { |i| break "hello" } # => "hello"
最初の呼び出しのときの値は、twice
メソッドが yield
されているためブロックの値となります。一方2番目の呼び出しでは break
が実行されているため、値が "hello" となっています。
もしある条件によって break する場合、そのメソッドの戻り値の型は、ブロックの戻り値の型とすべての break
の型の組み合わせとなります。
value = twice do |i|
if i == 1
break "hello"
end
i + 1
end
value # :: Int32 | String
break
が複数の引数を受け取るとき、それらは自動的に Tuple に変換されます。
values = twice { break 1, 2 }
values # => {1, 2}
break
が引数を取らない場合、nil
を1つ受け取ったのと同じことになります。
value = twice { break }
value # => nil
next¶
ブロックの中にnext
式があるとそこで、 (メソッドではなく) ブロックを抜けます。例をあげます。
def twice
yield 1
yield 2
end
twice do |i|
if i == 1
puts "Skipping 1"
next
end
puts "Got #{i}"
end
# Output:
# Skipping 1
# Got 2
next
式は引数を受け取ることが可能です。そのとき、受け取った値はそのブロックを実行した yield
式の値となります。
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
if i == 1
next 10
end
i + 1
end
# Output
# 10
# 3
next
が複数の引数を受け取るとき、それらは自動的に Tuple に変換されます。引数を取らない場合には、 nil
を1つ受け取ったのと同じことになります。
with ... yield¶
yield
式に with
キーワードを使うと、ブロック内でメソッドを実行する際にデフォルトのレシーバとなるオブジェクトを指定することができます。
class Foo
def one
1
end
def yield_with_self
with self yield
end
def yield_normally
yield
end
end
def one
"one"
end
Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one } # => "one"
ブロック引数のアンパック¶
括弧で囲むことでブロック引数をアンパックできます。
array = [{1, "one"}, {2, "two"}]
array.each do |(number, word)|
puts "#{number}: #{word}"
end
上の例は以下のシンタックスシュガーになります。
array = [{1, "one"}, {2, "two"}]
array.each do |arg|
number = arg[0]
word = arg[1]
puts "#{number}: #{word}"
end
整数の渡せる []
メソッドを持つどのような型のブロック引数に対してもアンパックは可能です。
For Tuple parameters you can take advantage of auto-splatting and do not need parentheses:
array = [{1, "one", true}, {2, "two", false}]
array.each do |number, word, bool|
puts "#{number}: #{word} #{bool}"
end
例えば Hash(K, V)#each はブロックに Tuple(K, V)
を渡すので、自動展開が上手く機能します。
h = {"foo" => "bar"}
h.each do |key, value|
key # => "foo"
value # => "bar"
end
パフォーマンス¶
ブロックを yield
するとき、そのブロックは常にインライン展開されます。クロージャやメソッド呼び出し、そして関数ポインタなどが使われることはありません。これは次のことを意味しています。
def twice
yield 1
yield 2
end
twice do |i|
puts "Got: #{i}"
end
上記は以下のように書くことと完全に同じです。
i = 1
puts "Got: #{i}"
i = 2
puts "Got: #{i}"
例えば、標準ライブラリには times
という整数型に対するメソッドがあり、それを使うと以下のように書くことができます。
3.times do |i|
puts i
end
とても読みやすいですが、これは C にループのように高速に動くのでしょうか?答えは YES です。
Int#times
は以下のように定義されています。
struct Int
def times
i = 0
while i < self
yield i
i += 1
end
end
end
補足されないブロック (non-captured block) は常にインライン展開されます。したがって、上記のメソッドの実行は、以下のように書くこととまったく同じです。
i = 0
while i < 3
puts i
i += 1
end
コードの読みやすさや再利用性のために積極的にブロックを利用しましょう。それが実行時のパフォーマンスに影響することはありません。