& とブロック

初めてのRuby を読んでいて、& の挙動を正しく理解してなかったので整理。

& 演算子

& 演算子とは以下のようなやつ。

def hoge(&b)
  b.call
end

hoge{1 + 1} #=> 2


& をメソッド定義時に、引数の前に書くと、呼び出し側のブロックを表すProc オブジェクトが格納される。逆に、以下のように呼び出し時に& を書くと、Proc オブジェクトがブロックになる。(Proc オブジェクトをブロックとしてブロック付きメソッド呼び出しを行う)

def fuga
  yield
end

proc = Proc.new{1 + 1}
fuga(&proc)


& 演算子っていう感じじゃないか。& 演算子と言えば、ビット演算とか配列の積集合の方を思い浮かべる。この& は、引数の修飾子って感じだ。 * と同じ。

>|ruby|
def hoge(*args)
  args
end

hoge(1, 2, 3) #=> [1, 2, 3]

def fuga(a, b, c)
  a + b + c
end

fuga(*[1, 2, 3]) #=> 6


* は、メソッド定義時に引数の前につければ、0個以上の引数を配列としてまとめて受ける。初めてのRuby の中には、多値を配列に変換すると書かれていた。
逆に、呼び出し時に* を書くと、配列が多値に変換される。「fuga(*[1, 2, 3])」は「fuga(1, 2, 3)」と同じになる。


* が配列化、引数展開だから、& は、Proc化、ブロック展開って感じだ。

& の挙動

定義側、呼び出し側両方に& がついていてもOK

* と同じで、定義側、呼び出し側両方に& がついていても上手くいく。呼び出し側は、Proc オブジェクトをブロックとしてメソッドを呼び出し、hoge は、そのブロックをProc オブジェクトとしてb に格納するから。

def hoge(&b)
  b.call
end

hoge(&lambda{1 + 1}) #=> 2
一般の引数とは異なる

& はあくまでブロックをProc に変換し、Proc をブロック展開する。なので、上のhoge メソッドは、0個の引数しか受け取らない。

hoge(1)
ArgumentError: wrong number of arguments (1 for 0)
	from (irb):3:in `hoge'
	from (irb):3
 
hoge(lambda{1 + 1})   #この場合は、&lambda.. としてやれば実行可能
ArgumentError: wrong number of arguments (1 for 0)
	from (irb):4:in `hoge'
	from (irb):4

(ハイライトがおかしい)


また、& をProc オブジェクト以外のオブジェクトに対して作用させてもエラーが発生する。

hoge(&1)
TypeError: wrong argument type Fixnum (expected Proc)
	from (irb):5

& 修飾子を上手く使ったSymbol#to_proc

上記の「& をProc オブジェクト以外のオブジェクトに対して作用させてもエラーが発生する」という部分に関して補足。& は、Proc オブジェクト以外にも適応できる場合があります。

&演算子はProcオブジェクトをブロックへ変換し、ブロックをProcオブジェクトへ変換します。この場合、&はシンボル :+ をブロックへ変換しようとします。ブロックへの変換にはRuby組み込みの型変換が利用されます。ブロックへの型変換ではまず、Procオブジェクトが与えられているかをチェックします。ブロックが与えられていなければ、引数として与えられたオブジェクトをProcへと変換すべく to_procメソッドが呼び出されます。このとき、シンボル :+ に to_proc メソッドが定義されていれば、それが呼び出されます。Ruby 1.9では、Symbol#to_roc が定義されています。このto_procメソッドは、Procオブジェクトを返します。
つまり、 &:+ は { |x, y| x + y }になるということです。


ここを参考にすると(これ、「ブロックが与えられていなければ」ってとこは、「Proc オブジェクトが与えられていなければ」だと思うけど)、メソッド呼び出し時の& の挙動は以下のようになる。


if . & の引数として(引数っていって良いのか?)、Proc オブジェクトが与えられている
 Proc オブジェクトをブロックとしてメソッドを呼び出す
else
 引数のオブジェクトのto_proc メソッドを呼び出し、結果として返ってきたProc オブジェクトをブロックとしてメソッドを呼び出す
end


最初は、& の引数がProc オブジェクトでもto_proc されるかとおもいきや、そうではなかった。

class Proc
  alias to_proc_without_alert to_proc
  
  def to_proc_with_alert
    p 'called'
    to_proc_without_alert
  end
  
  alias to_proc to_proc_with_alert
end

def hoge
  yield
end

hoge(&lambda{1 + 1})  #=> 2
Symbol#to_proc

RailsActiveSupport にあって、ruby1.9 に取り込まれたやつ。

class Symbol
  def to_proc
    lambda{|*args| args.shift.__send__(self, *args)}
  end
end


これを使うと、こう書ける。

  #before
  [1, 2].map{|e| e.to_s}

  #after
  [1, 2].map(&:to_s)

& によって、Symbol#to_proc が呼ばれ、そのProc をブロックとしてmap が実行される。


それにしても、Symbol#to_proc の*args がにくいね。これがあるから、次のような例も上手くいく。

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

inject はブロック引数を二つとるから、「args.shift.__send__(self, *args)」によって、第一引数 + 第二引数ってきちんとなってくれる。んーにくい!