【一人 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 &:upcase
は func { |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つ&obj
はobj.to_proc
がブロック引数として渡される&
を利用することでブロックを記述することなく簡潔にブロックを記述することが出来る&:upcase
は{ |it| it.upcase }
と同等。#method
を利用してトップレベルやKernel
のメソッドに引数として渡すことも出来る
Ruby を使い始めた頃は &
という記法が何をやっているのかよくわからないで混乱することが多かったですが、今では &
がないと Ruby のコードが書けないぐらいには依存しています。
{}
記法でブロックの処理を記述するのもいいですが、&:hoge
のように『&
+ Symbol
』を利用することでブロックをより簡潔に記述する事が出来るので利用してみるとよいと思います。