以下の&method
部分の意味が分からなかったので調べてみました。
['foo', 'bar', 'baz'].each(&method(:puts))
今回理解したいコードはこちらです。
class Hoge
def positive_even_list
even_filter(list)
.then(&method(:positive_filter))
end
def list
[10, -5, 0, 21, -30]
end
def even_filter(list)
list.filter(&:even?)
end
def positive_filter(list)
list.filter(&:positive?)
end
end
Hoge.new.positive_even_list
# => [10]
まとめ
- Procオブジェクトの前に
&
プレフィックスをつけると、ブロックとして扱われる - Procオブジェクトではなくても、
to_proc
メソッドが定義されているオブジェクトであれば、暗黙的にto_procを呼び出してProcオブジェクトへ変換される - これを使うことで、ブロックを省略することができる
-
Method#to_proc
とSymbol#to_proc
が便利
サンプルコード1 Symbol#to_proc
最初の例の&method
を理解する前に、比較的よく見る、Symbolのケースを使って説明していきます。
['foo', 'bar', 'baz'].map(&:upcase)
=> ["FOO", "BAR", "BAZ"]
このような使い方を目にすることがあると思います。
Array#each (Ruby 3.1 リファレンスマニュアル) を見ると、eachはeach {|item| .... } -> self
のように、ブロックを受け取り、各要素に対してブロックを評価するようです。
上記の書き方はブロックが省略されていますが、省略しないとこのようになります。
# ['foo', 'bar', 'baz'].map(&:upcase)
['foo', 'bar', 'baz'].map { |item| item.upcase }
=> ["FOO", "BAR", "BAZ"]
よく見るものの、どうしてこうなるのかが理解できていなかったのですが、実はこの変換には、Symbol#to_procが使われていました。
なぜ急にto_procが出てきたかを理解するために、上の例のブロック部分をProcオブジェクトに置き換えた例を見てみます。
# ['foo', 'bar', 'baz'].map { |item| item.upcase }
upcase_proc = lambda { |item| item.upcase }
['foo', 'bar', 'baz'].map(&upcase_proc)
Procオブジェクトの前に &プレフィックスをつけると、ブロックとして扱うことができます。
ブロックはオブジェクトではないのですが、Procオブジェクトにすることで、オブジェクトとして引数に渡したりすることができます。
上記の例では、 upcase_proc
というProcオブジェクトに対して&プレフィックスをつけたことで、ブロックとして扱うことができ、mapにブロックを渡すことができていました。
upcase_proc.class
=> Proc
ただ、先程の例では&プレフィックスのあとにあったのはProcオブジェクトではなく、Symbolでした。
['foo', 'bar', 'baz'].map(&:upcase)
このとき、&プレフィックスの後ろのオブジェクトにto_proc
メソッドが定義されていると、暗黙的に呼ばれて変換されるようになっています。
明示的に記述すると、
['foo', 'bar', 'baz'].map(&(:upcase.to_proc))
Symbol#to_procのリファレンスマニュアルには以下のように書かれています。
self に対応する Proc オブジェクトを返します。
生成される Proc オブジェクトを呼びだす(Proc#call)と、 Proc#callの第一引数をレシーバとして、 self という名前のメソッドを残りの引数を渡して呼びだします。
(中略):to_i.to_proc["ff", 16] # => 255 ← "ff".to_i(16)と同じ
Symbol#to_procにより、第1引数をレシーバとするProcオブジェクトになります。
これによって、SymbolがProcオブジェクトになり、&プレフィックスによってブロックとなり、mapでブロックが評価されています。
# ['foo', 'bar', 'baz'].map(&:upcase)
['foo', 'bar', 'baz'].map { |item| item.upcase }
サンプルコード2 Method#to_proc
&method
について理解を深めるために、シンプルな例として以下を用います。
['foo', 'bar', 'baz'].each(&method(:puts))
とは言っても先程の例と同様です。
&プレフィックスがあるため、以下のように暗黙的にto_procが呼ばれ、Procオブジェクトに変換されています。
['foo', 'bar', 'baz'].each(&method(:puts).to_proc)
Method#to_proc (Ruby 3.1 リファレンスマニュアル)を見てみると、
self を call する Proc オブジェクトを生成して返します。
とあります。つまり、以下のような意味になります。
method_proc = lambda { |arg| method(:puts).call(arg) }
['foo', 'bar', 'baz'].each(&method_proc)
このmethod
メソッドは、Object#methodで、
オブジェクトのメソッド name をオブジェクト化した Method オブジェクトを返します。
とあります。メソッドもブロックと同じようにオブジェクトではないのですが、Methodオブジェクトにすることでオブジェクトとして扱うことができます。
例えば、'foo bar'.reverse
をMethodオブジェクトにすると、
reverse_method = 'foo bar'.method(:reverse)
=> #<Method: String#reverse()>
reverse_method.call
=> "rab oof"
となります。よって、
# ['foo', 'bar', 'baz'].each(&method(:puts))
# ↓
# ['foo', 'bar', 'baz'].each(&method(:puts).to_proc)
# ↓
# ['foo', 'bar', 'baz'].each(&lambda { |arg| method(:puts).call(arg) })
# ↓
['foo', 'bar', 'baz'].each(&lambda { |arg| puts(arg) })
# ↓
['foo', 'bar', 'baz'].each { |arg| puts(arg) }
というように変換できます。
Symbol#to_procとMethod#to_procの使い分け
使い分けという程ではないですが、こんがらがりそうなので、並べてみると、
Symbol
# ['foo', 'bar', 'baz'].map(&:upcase)
['foo', 'bar', 'baz'].map { |item| item.upcase }
Method
# ['foo', 'bar', 'baz'].each(&method(:puts))
['foo', 'bar', 'baz'].each { |item| puts(item) }
となり、Symbolはそのitemをレシーバとして呼び出すとき、Methodは引数として呼び出す時に使えます。
最初のサンプルコード
ここまで来るともう読めるはずです。
class Hoge
def positive_even_list
even_filter(list)
.then(&method(:positive_filter))
+# .then { |list| positive_filter(list) }
end
def list
[10, -5, 0, 21, -30]
end
def even_filter(list)
list.filter(&:even?)
+# list.filter { |item| item.even? }
end
def positive_filter(list)
list.filter(&:positive?)
+# list.filter { |item| item.positive? }
end
end
Hoge.new.positive_even_list
# => [10]
このように使うことができます。
まとめ
- Procオブジェクトの前に
&
プレフィックスをつけると、ブロックとして扱われる - Procオブジェクトではなくても、
to_proc
メソッドが定義されているオブジェクトであれば、暗黙的にto_procを呼び出してProcオブジェクトへ変換される - これを使うことで、ブロックを省略することができる
-
Method#to_proc
とSymbol#to_proc
が便利
おまけ
&method
のように、&
とmethod
が一体となっているわけではないので、以下のようにも使えます。
require 'json'
'{"hoge":"fuga"}'
.then(&JSON.method(:parse))
=> {"hoge"=>"fuga"}
JSONを取得してパースするときにブロックを省略して書くこともできます。 (ブロックを書いたほうがわかりやすいです)