5
3

More than 1 year has passed since last update.

Method#to_procを利用して、ブロックを省略する方法

Posted at

以下の&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_procSymbol#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_procSymbol#to_procが便利

おまけ

&methodのように、&methodが一体となっているわけではないので、以下のようにも使えます。

require 'json'

'{"hoge":"fuga"}'
  .then(&JSON.method(:parse))
=> {"hoge"=>"fuga"}

JSONを取得してパースするときにブロックを省略して書くこともできます。 (ブロックを書いたほうがわかりやすいです)

参考

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3