Ruby において面食らう記法の一つは、map
メソッドなどの引数に現れる、&:upcase
というような、以下の3つの要素が同時に存在する記法だと思います*1。
- アンド(アンパサンド)
- コロン
- 文字
- 上記の例では
upcase
- 上記の例では
改めてこの記法について整理してみます。箇条書きに番号が振ってありますが、必ずしも番号順に理解を要するものではありません。
1. 「コロン」と「文字」は合体して、「シンボル」として読む
まず「コロン」と「文字」の2つの要素ですが、これはこの2つの要素を合体し、「シンボル」として読みます*2。上記の例では :upcase
が一つのかたまりの要素となります。
これで、全体の要素は3つから2つになりました*3。
2. 「コロン」と「文字」における「文字」部分の名称には、「メソッド名」が来る
「コロン」と「文字」は上記のように「シンボル」としてまとめました。このうち「文字」の部分に来る名称には「メソッド名」が来ます。厳密には、「レシーバに対して適用したいメソッド名」が来ます。
上記の例では upcase
が「メソッド名」になっています*4。
3. 「アンド(アンパサンド)」 + 「シンボル」という記述は、そのまとまりが Proc オブジェクトであることを示している
ここらへんから話が込み入ってきます。最初は流し読みで構いません。
&
と「シンボル」を連ねた記述は、そのまとまりが Proc オブジェクト であることを示しています。「シンボル」に対して to_proc
メソッドを実行した記述と「概念的には」同じです*5。
つまり以下の2つの記述は、概念的には、同じことを示しています。
&:upcase
:upcase.to_proc
4. なぜ「&:upcase」のように、「アンド(アンパサント)」が付いているか(「アンド(アンパサント)の意味は何なのか)
&:upcase
という記法において &
が付いている理由は、「それを付けることで、引数がブロックであることを明示するから」です。
&:upcase
という記法は「引数として渡すときに記述する記法」であるので、たとえば irb でいきなりこの記法で書いた場合は SyntaxError
になります*6。「3.」において「概念的には」と書いた理由はこのことに基づきます。
> &:upcase Traceback (most recent call last): 3: from /FOOBAR/.rbenv/versions/2.7.0/bin/irb:23:in `<main>' 2: from /FOOBAR/.rbenv/versions/2.7.0/bin/irb:23:in `load' 1: from /FOOBAR/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>' SyntaxError ((irb):9: syntax error, unexpected &) &:upcase ^
5. :upcase というシンボルが、to_proc というメソッドを実行できているのに違和感がある
私も初めて見たときは違和感がありまくりでしたが、Ruby の仕様上、シンボルは to_proc
メソッドを実行することができます。つまり、仕様です。
6. シンボルから生成した Proc オブジェクトは、シンボル名と同じメソッドを実行するための Proc オブジェクト になる
例えば「:upcase.to_proc
という Proc オブジェクト は、upcase
メソッドを実行するための Proc オブジェクト である」ということです。つまり、以下のような Proc オブジェクト となります。
> Proc.new {|foobar| foobar.upcase }
「メソッドを実行するための Proc オブジェクト」というあいまいな表現を用いていますが、これは引数の内容が関わってくるためなので(「7.」を参照)、このように表現しました。
7. シンボルから生成した Proc オブジェクトの「第一引数」は、「レシーバ」となる
「6.」より、「シンボルから生成した Proc オブジェクトは、シンボル名と同じメソッドを実行するための Proc オブジェクト」となることが分かりました。この Proc オブジェクト を実行する(call
メソッドを実行する)際の「第一引数」が「レシーバ」となります*7。
「6.」の内容と合わせて考えると、すなわち「第一引数に対して、『シンボル名のメソッド』を実行する」ということになります。これは具体的なコードを示すと分かりやすいでしょう。以下のコードをご覧ください。
> :upcase.to_proc.call('abcdefg') => "ABCDEFG"
上記のコードは以下のコードと同じ、ということです。
> sample_proc = Proc.new {|foobar| foobar.upcase } > sample_proc.call('abcdefg') => "ABCDEFG"
8. これで &:upcase 記法が理解できる
これまでの内容を押さえれば、&:upcase
記法が理解できます。以下、具体的なコード例とともに整理します。
例として挙げるコードの仕様は次のとおりです。
['abc', 'def', 'ghi', 'jkl']
という配列をレシーバとして用意する- 上記の配列に対して、
map
メソッドを実行する - その結果、戻り値として
['ABC', 'DEF', 'GHI', 'JKL']
が返ってくる
具体的なコードです。
> ['abc', 'def', 'ghi', 'jkl'].map(&:upcase) => ["ABC", "DEF", "GHI", "JKL"]
順を追って理解していきます。
- まず、
map
メソッドにより、配列の各要素が一つ一つ順番に&:upcase
に渡される - そして、
&:upcase
は、配列の各要素を一つ一つ順番に引数として受け取る - ここで、
&:upcase
が行うことは、受け取った引数に対して:upcase.to_proc.call(引数の値)
を実行することと同義である- すなわち、例えば 'abc' という引数に対して 'ABC' という戻り値を返す
map
メソッドは、「『上記により得られた戻り値』を要素とした配列」を戻り値として返す
上記の内容により、['abc', 'def', 'ghi', 'jkl'].map(&:upcase)
という記法が理解できたのではないでしょうか。
補足
W. 正確性について
理解できることを優先したため、内容の厳密な正確さを犠牲にした箇所があります。
X. 内容を理解するための前提の知識
上記の内容を理解するためには、以下の項目の知識が必要となります。
- レシーバ
- 戻り値
- map メソッド
- ブロック
- ブロック引数
- シンボル
- Proc オブジェクト
to_proc
メソッドcall
メソッド
Y. なぜこのような(初見では難読である)書き方をするのか
Ruby っぽいから、だと思っています。
あとは RuboCop が自動でこの記法に修正してくれるから、という理由もあるかと思います。
Z. この記法が使える場合は限られる
これまでの内容より、この記法が使える場合は限られます。具体的には、以下のすべての条件を満たす場合にのみ、この記法を使うことができます(後述の「プロを目指す人のためのRuby入門」の 「4.4.5」より)。
- ブロック引数が1個だけである
- ブロックの中で呼び出すメソッドには引数がない
- ブロックの中では、ブロック引数に対してメソッドを1回呼び出す以外の処理がない
なぜ上記の条件が必要であるかは、これまでの内容を踏まえると分かると思います。
参考
「プロを目指す人のためのRuby入門」を参考にしました。名著です。