【一人 Ruby Advent Calendar 2017】Ruby で & を使ってブロック引数を渡す【5日目】

一人 Ruby Advent Calendar 2017 5日目の記事になります。
今回は前回書いた『ブロックについていろいろ【4日目】』の続きになります。

前回のおさらい

さてさて、Ruby では以下のようにしてブロック引数という特別な引数を使うことができます。

def func &block
    # block は必ず Proc オブジェクト
    p block

    # ブロックの処理を呼び出す
    block.call 1, 2
end

p func { |a, b| a + b }
# => 3

このようにブロック引数を {} で定義して渡している事が分かりますね。

& でブロック引数を渡す

Ruby では以下のように『& 含めた引数』を渡しているコードをよく見かけます。

p ["homu", "mami", "mado"].map &:upcase
# => ["HOMU", "MAMI", "MADO"]

実はこれも『&:upcase をブロック引数に渡している』というようなメソッド呼び出しになります。

& は何をやっているのか

では & は何をやっているのでしょか。
これは func &obj とした場合に『func メソッドに obj.to_proc をブロック引数に渡している』というような処理になります。
つまり、&:upcase というのは :upcase.to_proc の結果をブロック引数に渡していることになります。

def func &block
    # :hoge.to_proc をブロック引数として受け取る
    p block
    # => #<Proc:0x00000000019c4a18(&:hoge)>

    # :hoge.to_proc と同じ
    p :hoge.to_proc
    # => #<Proc:0x0000000001149960(&:hoge)>
end

func &:hoge

また、これは :upcase のような Symbol オブジェクトに限らず、#to_proc メソッドが定義されているオブジェクトであればなんでも渡すことができます。

class X
    def to_proc
        # Proc オブジェクトを返す
        proc {
            "X#to_proc"
        }
    end
end


def func &block
    p block
    # => #<Proc:0x0000000001328e48@/tmp/vXw2j9T/671:4>

    # X#to_proc が呼び出される
    p block.call
    # => "X#to_proc"
end

x = X.new

# x.to_proc をブロック引数として渡す
func &x

ただし、#to_proc メソッドは Proc オブジェクトを返す必要があるので注意してください。

class X
    def to_proc
        42
    end
end


def func &block
    # ...
end

x = X.new

# Error: can't convert X to Proc (X#to_proc gives Integer) (TypeError)
# to_proc は Proc オブジェクトを返す必要がある
func &x

Symbol#to_proc は何をやっているのか

さて、&:upcase:upcase.to_proc を呼び出していることはわかったと思います。 では、:upcase.to_proc 自体は何をやっているのでしょうか。 Symbol#to_proc は『引数に対してそのシンボル名のメソッドを呼び出す Proc オブジェクトを返す』という処理になります。 どういうことかというと

:upcase.to_proc.call "homu"

"homu".send(:upcase)
# つまり
# "hoge".upcase

というような呼び出しと同等になります。 また、to_proc.call に対して引数が複数ある場合は、『第二引数以降は呼び出すメソッドの引数』として渡されます。

# このように複数の引数がある場合は
p :+.to_proc.call 1, 2
# => 3

# 以下と同じ
p 1.send(:+, 2)
# => 3

# 以下と同じ
p 1.+ 2
# => 3

まあ、要約すると func &:upcasefunc { |it| it.upcase } と同じということですね。

いろいろと使ってみる

上記を踏まえると次のようなコードは & を使ってより簡潔に記述することができます。

p ["homu", "mami", "mado"].map { |it| it.capitalize }
# => ["Homu", "Mami", "Mado"]

p (1..10).select { |it| it.even? }
# => [2, 4, 6, 8, 10]

p (1..10).inject { |a, b| a + b }
# => 55

これらを & + Symbol で記述すると以下のようになります。

p ["homu", "mami", "mado"].map &:capitalize
# => ["Homu", "Mami", "Mado"]

p (1..10).select &:even?
# => [2, 4, 6, 8, 10]

p (1..10).inject &:+
# => 55

こんな感じでだいぶ簡潔に記述することができました。

#method を活用する

最後に Symbol ではなくて #method メソッドをブロック引数として渡してみましょう。
#method メソッドは『レシーバのメソッドをオブジェクトとして返す』メソッドになります。

plus3 = 3.method(:+)
p plus3
# => #<Method: Integer#+>

p plus3.to_proc.call 4
# => 7

# plus3.call と直接 call を呼び出すことも出来る
p plus3.call 4
# => 7

このように Method オブジェクトを返します。
また、このオブジェクトも自身を呼び出す Method#to_proc を返すので以下のようにブロック引数として使用することもできます。

p (1..5).map &2.method(:+)
# => [3, 4, 5, 6, 7]

# 以下のように記述するのと同じ
p (1..5).map { |it| 2 + it }
# => [3, 4, 5, 6, 7]

このように &2.method(:+){ |it| 2 + it } と同じ処理になります。
これを利用して次のようにしてトップレベルや Kernel のメソッドに引数として渡すこともできます。

def fizzbuzz n
      n % 15 == 0 ? "FizzBuzz"
    : n %  3 == 0 ? "Buzz"
    : n %  5 == 0 ? "Fizz"
    : n
end

p method(:fizzbuzz).call 5
# => Fizz

# 要素を fizzbuzz に引数として渡す
p (1..20).map &method(:fizzbuzz)
# => [1, 2, "Buzz", 4, "Fizz", "Buzz", 7, 8, "Buzz", "Fizz", 11, "Buzz", 13, 14, "FizzBuzz", 16, 17, "Buzz", 19, "Fizz"]

# puts メソッドを呼び出したり
["homu", "mami", "mado"].each &method(:puts)
# => homu
# mami
# mado

このように &method(:fizzbuzz){ |it| fizzbuzz it } と同じ処理になります。

まとめ

  • &obj はブロック渡しの1つ
  • &objobj.to_proc がブロック引数として渡される
  • & を利用することでブロックを記述することなく簡潔にブロックを記述することが出来る
  • &:upcase{ |it| it.upcase } と同等。
  • #method を利用してトップレベルや Kernel のメソッドに引数として渡すことも出来る


Ruby を使い始めた頃は & という記法が何をやっているのかよくわからないで混乱することが多かったですが、今では & がないと Ruby のコードが書けないぐらいには依存しています。
{} 記法でブロックの処理を記述するのもいいですが、&:hoge のように『& + Symbol』を利用することでブロックをより簡潔に記述する事が出来るので利用してみるとよいと思います。

参照