Ruby の &:upcase 記法を改めて整理してみる

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入門」を参考にしました。名著です。

gihyo.jp

*1:この記法は、ブロックを引数として取るメソッドが出てくると、たまに現れます

*2:ハッシュにおけるキーバリューを区分するためのコロンではない、ということです

*3:「アンド(アンパサンド)」と「シンボル」の2つ

*4:レシーバは String オブジェクト になることを想定しています

*5:厳密に同じでなく、「概念的には」と書いた理由は、「4.」に記しています

*6::upcase.to_proc の場合は、Proc オブジェクト が正しく戻り値として得られる

*7:本題とは関係がない発展事項となりますが、「第二」以降の引数も取ることができます

Powered by はてなブログ