ブロックとProc¶
メソッドは、`yield`キーワードで実行されるコードブロックを受け入れることができます。例えば
def twice(&)
yield
yield
end
twice do
puts "Hello!"
end
上記のプログラムは、各`yield`に対して一度ずつ、「Hello!」を2回出力します。
ブロックを受け取るメソッドを定義するには、`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
# The above is the same as
foo(bar) do
something
end
foo bar { something }
# The above is the same as
foo(bar { something })
これは、`do ... end`を使用してドメイン固有言語(DSL)を作成し、それらを普通の英語として読むことができるようにするためです。
open file "foo.cr" do
something
end
# Same as:
open(file("foo.cr")) do
something
end
上記のようにしたくありません。
open(file("foo.cr") do
something
end)
オーバーロード¶
yieldするメソッドとしないメソッドの2つは、「オーバーロード」のセクションで説明されているように、異なるオーバーロードと見なされます。
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 is Int32 | Bool
# second is 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
と
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]
ダミーの変換メソッド
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」を出力します。`thrice`メソッドは、`break`のために`puts "Before 3"`式を実行しませんでした。
`break`は引数も受け取ることができます。これらはメソッドの戻り値になります。例えば
def twice(&)
yield 1
yield 2
end
twice { |i| i + 1 } # => 3
twice { |i| break "hello" } # => "hello"
最初の呼び出しの値は3です。なぜなら、`twice`メソッドの最後の式は`yield`であり、ブロックの値を取得するからです。2番目の呼び出しの値は「hello」です。なぜなら、`break`が行われたからです。
条件付きbreakがある場合、呼び出しの戻り値の型は、ブロックの値の型と多くの`break`の型のユニオンになります。
value = twice do |i|
if i == 1
break "hello"
end
i + 1
end
value # :: Int32 | String
`break`が複数の引数を受け取る場合、それらは自動的にタプルに変換されます。
values = twice { break 1, 2 }
values # => {1, 2}
`break`が引数を受け取らない場合、単一の`nil`引数を受け取るのと同様です。
value = twice { break }
value # => nil
`break`が複数のネストされたブロック内で使用される場合、直近の包含ブロックのみが中断されます。
def foo(&)
pp "before yield"
yield
pp "after yield"
end
foo do
pp "start foo1"
foo do
pp "start foo2"
break
pp "end foo2"
end
pp "end foo1"
end
# Output:
# "before yield"
# "start foo1"
# "before yield"
# "start foo2"
# "end foo1"
# "after yield"
2つの`"after yield"`や`"end foo2"`は取得されません。
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
引数を受け取るのと同様です。
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
つまり、整数で[]
に応答する任意の型は、ブロックパラメータで展開できます。
パラメータの展開はネストできます。
ary = [
{1, {2, {3, 4}}},
]
ary.each do |(w, (x, (y, z)))|
w # => 1
x # => 2
y # => 3
z # => 4
end
スプラットパラメータがサポートされています。
ary = [
[1, 2, 3, 4, 5],
]
ary.each do |(x, *y, z)|
x # => 1
y # => [2, 3, 4]
z # => 5
end
Tupleパラメータの場合、自動スプラットを利用でき、括弧は必要ありません。
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のforループと同じくらい高速ですか?答えは:はい!
これはInt#times
の定義です。
struct Int
def times(&)
i = 0
while i < self
yield i
i += 1
end
end
end
キャプチャされていないブロックは常にインライン化されるため、上記のメソッド呼び出しは、次のように記述するのと同じです。
i = 0
while i < 3
puts i
i += 1
end
可読性やコードの再利用のためにブロックを使用することを恐れないでください。結果の実行ファイルのパフォーマンスに影響を与えることはありません。