晴耕雨読

working in the fields on fine days and reading books on rainy days

[Ruby] 配列を返すメソッドをブロックも受け付けるように拡張する

Rubyで配列を返すメソッドをブロックも受け付けるように拡張する方法について説明します。

問題

ここでは問題の単純化の為に、例としてソートした配列を返すメソッド sort_array というのがあるとする。

def sort_array(array)
  array.sort
end

このとき、

members = ['Alice', 'Carol', 'Bob', 'Dave']

sorted = sort_array(members)
# => ['Alice', 'Carol', 'Bob', 'Dave']

と書けば配列を返すメソッドとなるが、ブロック(do ~ end)が渡されるとまるで Enumerator のように 中身を一つずつ引数に渡すような関数を作りたいとする。

sort_array(members) do |elem|
  puts elem
end
# >> Alice
# >> Carol
# >> Bob
# >> Dave
# => ['Alice', 'Carol', 'Bob', 'Dave']

block_given?, yield

Ruby には Kernel#block_given? というメソッドがあり、関数にブロックが渡されると、true を返す。 yield というキーワードは、与えられたブロックに対して引数を渡す。

これらを使って、例の sort_array を再実装すると次のようになる。

def sort_array(array)
  array.sort.tap do |sorted|
    sorted.each { |elem| yield elem } if block_given?
  end
end

Object#tap は、ブロックに自分自身(self)を渡し、自分自身を返す面白いメソッドである。 もし、Object#tap を使わないでこれを書く場合は、次のように書く。

def sort_array(array)
  sorted = array.sort
  sorted.each { |elem| yield elem } if block_given?
  sorted
end

Object#tap を使った方が、変数のスコープが小さくなるというメリットがある(この例の場合では sorted という変数)。 しかし、ネストが増えるというデメリットもある。今回はネストが深いわけではないので、積極的に Object#tap を使っていくこととする。

&block

加えてさらに、改善できる点がある。 .each { |elem| yield elem } というコードは、each から渡された要素を、 sort_array 関数が受け取ったブロックに流しているだけである。 ブロック(block)からプロック(Proc)への変換は単項演算子 & を使えばいいという点と、 each のような Enumerable を扱うメソッドは、引数としてプロック(Proc)を受け取れることを考慮すれば、 sort_array 関数のブロックを each のプロックとして使えることがわかる。

def sort_array(members, &block) # 明示的にブロックを引数としてとる
  members.sort.tap do |sorted|
    sorted.each(&block) if block
  end
end

まとめ

したがって、配列を返すメソッドをブロックも受け付けるように拡張するメソッドは最終的には次のように書けば良い。

members = ['Alice', 'Carol', 'Bob', 'Dave']

def sort_array(array, &block)
  array.sort.tap do |sorted|
    sorted.each(&block) if block
  end
end

sorted = sort_array(members)
# => ['Alice', 'Carol', 'Bob', 'Dave']

sort_array(members) do |member|
  puts member
end
# >> Alice
# >> Carol
# >> Bob
# >> Dave
# => ['Alice', 'Carol', 'Bob', 'Dave']

以上です。