Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: map(&:upcase) の &:upcase って何

こんにちは。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つの仕様の組み合わせで実現されていました。
積極的に使って行きたいと思います



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。