コンテンツへスキップ

ブロックと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#mapEnumerable#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

可読性やコードの再利用のためにブロックを使用することを恐れないでください。結果の実行ファイルのパフォーマンスに影響を与えることはありません。