こんにちは。kazzです。
以下のような実装を見かけたり、実際に使っているという方が多いのではないでしょうか。
%w[a b c].map(&:upcase) #=> ['A', 'B', 'C']
本日は、この実装がなぜ動くのかの秘密に迫りたいと思います。
メソッドにブロックを渡す
ブロック引数
rubyの任意のメソッドはブロックを受け取ることができ、yield
でメソッドに与えられたブロックを実行します。
def double
yield * 2
end
# do-end をブロック引数に指定する
double do
1 + 2 + 3 + 4 + 5 + 6
end
# => 42
# { ... } をブロック引数に指定する
double { 21 } #=> 42
ブロック引数には proc
を指定することができ、この場合は変数名の頭に &
をつけ、引数の最後に指定します。
one_to_six_proc = proc { 1 + 2 + 3 + 4 + 5 + 6 }
one_to_six_proc.call #=> 21
double(&one_to_six_proc) #=> 42
ブロック変数
仮引数の最後の変数名に &
をつけることで、与えられたブロックを変数として受け取ることができます。
def double(&block)
block.call * 2
end
double { 21 } #=> 42
ブロック引数に シンボル を指定する
以下のようにブロック引数として「シンボル」を指定することができます。
# 文字列リストを大文字の文字列リストに変換する
%w[a b c].map(&:upcase) #=> ["A", "B", "C"]
この実装は「なぞのへんかん」により、以下と等価になります。
# 文字列リストを大文字の文字列リストに変換する
%w[a b c].map{ |ch| ch.upcase } #=> ["A", "B", "C"]
なぞのへんかん の詳細
Symbol#to_proc
「なぞのへんかん」の詳細の前に Symbol#to_proc を把握しておく必要があります。
self
に対応するProc
オブジェクトを返します。
生成されるProc
オブジェクトを呼びだす(Proc#call
)と、Proc#call
の第一引数をレシーバとして、self
という名前のメソッドを残りの引数を渡して呼びだします。
生成されるProc
オブジェクトは lambda です。
Symbol#to_proc
(Ruby 3.1 リファレンスマニュアル)より
とありますので、以下のような実装になっていると思われます(擬似コード)。
class Symbol
def to_proc
lambda do |receiver, *args, **kwargs, &block|
receiver.public_send(self, *args, **kwargs, &block)
end
end
end
例えば :upcase.to_proc
は
lambda { |receiver| receiver.public_send(:upcase) }
となります。
なぞのへんかん のステップ実行
具体的に &:upcase
がどのように評価されるかを見てみましょう。
※わかりやすさを重視しているため、実際にrubyが評価する順番とは異りますのでご注意ください。
['a', 'b', 'c'].map(&:upcase)
↓
ブロック引数に対して暗黙的に #to_proc
が呼び出されます。
['a', 'b', 'c'].map(&(:upcase.to_proc))
↓
do
-end
ブロックで書き下すと以下のようになります。
['a', 'b', 'c'].map do |ch|
:upcase.to_proc.call(ch)
end
↓
わかりやすくするために map
を展開します。
[
:upcase.to_proc.call('a'),
:upcase.to_proc.call('b'),
:upcase.to_proc.call('c')
]
↓
:upcase.to_proc
を評価します。
[
lambda { |obj| obj.public_send(:upcase) }.call('a'),
lambda { |obj| obj.public_send(:upcase) }.call('b'),
lambda { |obj| obj.public_send(:upcase) }.call('c')
]
↓
lambda#call
を評価します。
[
'a'.public_send(:upcase),
'b'.public_send(:upcase),
'c'.public_send(:upcase)
]
↓
#public_send
を書き下します。
[
'a'.upcase,
'b'.upcase,
'c'.upcase
]
↓
#upcase
が評価されると以下のリストが得られます。
[
'A',
'B',
'C'
]
#to_proc
が実装されていればブロック引数に指定できる
ブロック変数に与えられたオブジェクトには暗黙的に #to_proc
が実行されるということは、#to_proc
が実装されているオブジェクトであればブロック引数として指定できることになります。
以下の例で確認してみます。
def double(&block)
block.call * 2
end
class MyNumber
def to_proc
proc { 21 }
end
end
my_number = MyNumber.new
double(&my_number) #=> 42
クラスでも問題ありません。
class Upcase
def self.to_proc
proc { |ch| ch.upcase }
# :upcase.to_proc # でもOK
end
end
%w[a b c].map(&Upcase) #=> ['A', 'B', 'C']
まとめ
list.map(&:upcase)
は専用の特殊構文ではなく
- 任意のメソッドは、ブロック引数に対して暗黙的に
#to_proc
を実行する - Symbolには
#to_proc
が実装されていて、レシーバーのメソッドを実行するlambdaを返す
という2つの仕様の組み合わせで実現されていました。
積極的に使って行きたいと思います