Rubyの&:の意味

結論

ブロック処理をより完結に書ける記法。

  • インスタンス.{ |ブロックパラメータ| ブロックパラメータ.メソッド名 }(&:メソッド名) で書ける
  • &:メソッド名 で記述するには特定の条件がある
  • 通称は「あんころ」らしい。

記述例

文字列の入った配列をupcase メソッドで大文字化する処理です。 ブロックを使った記述をを&: で置き換えることができます。

# ブロック使った記述
["a","b","c"].map{ |s| s.upcase }
=> ["A", "B", "C"]

# &:を使った記述
["a","b","c"].map(&:upcase)
=> ["A", "B", "C"]

{ |s| s.upcase } の部分を(&:upcase) に置き換えることができます。

&:メソッド名で書ける条件

以下の条件を満たす場合のみ&:メソッド名 の記述法を使うことができます。 詳しくは「プロを目指すためのRuby入門 4.4.5(p114)」に書かれています。

条件1: ブロックパラメータが1つ

ハッシュに対してブロック処理を行う際はブロックパラメータがキーと値の2つになりますが、ハッシュに対してブロック処理を行う際にブロックパラメータを2つ使って処理をする場合が多いと思いますが、このような場合は&:メソッド名 という書き方はできないようです。

例) ハッシュ内の値(value)のみ大文字にする

# OK
{key_1: "a", key_2: "b", key_3: "c"}.map{ |key, value| value.upcase }
=> ["A", "B", "C"]

# NG
{key_1: "a", key_2: "b", key_3: "c"}.map(&:upcase)
=> undefined method `upcase' for [:key_1, "a"]:Array (NoMethodError)

余談

ハッシュのブロック処理のブロックパラメータの数は必ずキーの2つという決まりは無いようです。

ブロックパラメータ内の値は配列型になっており、ハッシュのブロック処理のブロックパラメータは1つでも正しく動作します。 以下はハッシュのブロックパラメータが1つの時のブロックパラメータの値をコンソールで確認したものです。

> hash = {key_1: "a", key_2: "b", key_3: "c"}
=> {:key_1=>"a", :key_2=>"b", :key_3=>"c"}

# ハッシュでもブロックパラメータは1つでもok
> hash.map{|kv| kv}
=> [[:key_1, "a"], [:key_2, "b"], [:key_3, "c"]]

# ブロックパラメータは配列型で値が代入されている
> hash.map{|kv| kv.class}
=> [Array, Array, Array]

ブロックパラメータkv は配列型で値が保持されています。 ブロックパラメータが2つの場合はこの配列型の値をブロックパラメータのキーと値の2つにそれぞれ代入されているらしいです。

参照: https://pedantic-bardeen-b14a83.netlify.app/1

「ハッシュのブロック処理のブロックパラメータは必ず2つ」という間違った認識を修正できて良かったです。 @haruguchi_yumaさんアドバイスありがとうございました!

条件2: シンボルで渡すメソッドに引数が無い

シンボルで渡すメソッドに引数がある場合は&:メソッド名 という書き方はできないようです。

例) 配列内の文字列を空白で分割する

# OK
["a a", "b b", "c c"].map{ |s| s.split(" ") }
=> [["a", "a"], ["b", "b"], ["c", "c"]]

# NG
["a a", "b b", "c c"].map(&:split(" "))
=> SyntaxError

条件3: ブロックパラメータに対する処理が1つのメソッドのみ

ブロックパラメータに対する処理が1つのメソッドのみでなければなりません。

例) 配列内の文字列を大文字化、空白で分割する

# ブロックパラメータに対するメソッドが複数なので書き換えNG
["a a", "b b", "c c"].map{ |s| s.upcase.split }
=> [["A", "A"], ["B", "B"], ["C", "C"]]

なぜ&:メソッド名で書き換えることができるのか?

例) 配列内の文字列を大文字化する

# ブロック使った記述
["a","b","c"].map{ |s| s.upcase }
=> ["A", "B", "C"]

# &:を使った記述
["a","b","c"].map(&:upcase)
=> ["A", "B", "C"]

理由1: イテレータメソッドに&付きでシンボルを渡すと自動でProc化されるから

イテレータメソッド(eachmap 等)はブロックを受け取ることを期待します。 そしてイテレータメソッドはブロック以外のオブジェクトが渡った場合、ブロック以外で受け取ることができるオブジェクトへの変換を試みます。 この時にブロックから変換されたオブジェクトがProcオブジェクトです。 この自動変換処理はイテレータメソッドに必ずブロックを渡すためのRubyの仕様のようです。

イテレータメソッドに& 付きでシンボルを渡すと暗黙的にto_proc メソッドが呼ばれ、シンボルはProcオブジェクト化されます。 つまり、&:upcase:upcase.to_proc と同じ意味になります。 結果的にただのシンボル からブロック に変換されたことになり、内部的には{ |ブロックパラメータ| ブロックパラメータ.メソッド } と同じ形になっているのだと思います。

手続きオブジェクト(Proc)とは

ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

メソッド呼び出し(super・ブロック付き・yield) (Ruby 1.9.3)

ブロックの部分だけを先に定義して変数に保存しておき、後からブロック付きメソッドに渡すことも出来ます。 それを実現するのが手続きオブジェクト(Proc)です。

class Proc (Ruby 3.2 リファレンスマニュアル)

ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

class Proc (Ruby 3.2 リファレンスマニュアル)

Rubyはブロック処理を変数に代入することはできませんが、Procオブジェクトにのインスタンスに変換することでブロック処理をシンボル(:シンボル名) で扱うことができます。 Procオブジェクトはブロック処理を使い回すためのオブジェクトなのだと思います。

余談

to_proc メソッドに手を加えることで& にはシンボル以外の値も渡せるみたいです。 以下はto_proc の引数にメソッド名を文字列を渡せるようにメソッドを再定義しています。

class String
  def to_proc
    Proc.new { |obj| eval "obj.#{self}" }
  end
end

['a', 'b', 'c'].map(&'upcase') # 文字列のupcaseを渡す
=> ['A', 'B', 'C']

イテレータメソッドに&'文字列 でメソッド名を文字列で渡せているのが分かると思います。 Rubyの柔軟さがとても良く分かる面白い例ですね!

引用: https://pedantic-bardeen-b14a83.netlify.app/15?clicks=4

理由2: Procオブジェクトに引数を渡すとシンボルと同名のメソッドを自動で呼び出すから

イテレータメソッドに& 付きでシンボルを渡し、Procオブジェクト化される際にシンボルと同名のメソッドを自動で呼び出す仕様になっているようです。 つまり、&:upcaseupcase メソッドを呼び出します。

  1. map メソッドで配列内から一つずつ値を取り出す
  2. 取り出した値に対してupcase メソッドを実行
  3. 処理した値をmap メソッドで配列として返す

結果的にブロックを使った記述と同じ処理になるので&:メソッド名 で書き換えできるのだと思います。

感想

前に調査した際のメモが残っていたのでもったいないので記事にしてみたシリーズ第二弾(笑) 古いメモを記事に残すのは良い復習になるかもしれない。 Rubyはほんとに奥が深い...

参照