👻

Ruby の array.map(&method(:symbol)) ← これの動きについて理解したので備忘録

2024/12/22に公開

最近業務でほぼ初めて Ruby を扱うことになりました。
コードを読んでいる中で表題のようなコードを見かけ、初見で「何これ」となりました。
結構時間をかけて理解したので、備忘録も兼ねて記載しておきたいと思います。
※ 自分の理解なので間違っていたらコメントでフィードバックいただけると助かります。

結論

map メソッドを呼んでいる配列(array)にの各要素を、メソッド名が symbol の第一引数に渡して実行し、その結果を要素にした配列を返してくれます。

、、、言葉にすると分かりづらいですね。

例えば、[1,2,3].map(&method(:p)) を irb で実行してみます。

irb(main):012> [1,2,3].map(&method(:p))
1
2
3
=> [1, 2, 3]

ちょっと分かりづらいですが、
p メソッドは、第一引数を標準出力に出力した上で、第一引数を返り値とします。

そのため、map メソッドを呼んだ配列の各要素を標準出力に出力した上で、再度同じ配列を返してくれます。

で、なんでこうなるのか

結論に書いたとおり、

&method(:symbol) における symbol と同じメソッドを第一引数にして処理する。

と覚えるのは簡単ですが、なぜそのような動きになるのかが理解できずモヤモヤしておりました。
私がこの書き方に遭遇したのは、Ruby on Rails のモデルのクラスの中でした。

# コードは適当です
class Hoge < ApplicationRecord
  # ローカル変数はそれぞれ初期化処理の中で定義されているものとします
  def hoge
    @hoge = @array.map(&method(:fuga))
  end

  def fuga(arg)
    # 引数 arg を使った何かしらの処理
  end
end

まぁ、書き方でなんとなく動きは予想できましたし、実際にそう動いたのだからそれで良いじゃんとも思いましたが、せっかくなので理解した内容を書いておきます。

以下について分解した上でそれぞれを理解すると、このコードの動き全体を理解できるようになるかと思います。

  • Object#method の動きについて
  • & の演算子について
  • map メソッドの動きについて

Object#method の動きについて

まずは Object#method メソッドについてです。
こちらは、method メソッドの 呼び出し元 (レシーバ)と、引数のシンボルと同名のメソッドの組み合わせを
Method オブジェクトして保持し、好きなタイミングで実行できるようにするためのものです。

例えば、irb で以下のように class を定義し、method で Method オブジェクトを作成してみます。

irb(main):025* class Hoge
irb(main):026*   def hoge(arg)
irb(main):027*     puts "hello #{arg}"
irb(main):028*   end
irb(main):029> end
=> :hoge
irb(main):030> h = Hoge.new
=> #<Hoge:0x0000ffff8f04dbb8>
irb(main):031> m = h.method(:hoge)
=> #<Method: Hoge#hoge(arg) (irb):26>
irb(main):032> m.call("world")
hello world
=> nil

上記の m には、

  • 呼び出し元:Hoge.new で作成されたインスタンス (h)
  • メソッド:hoge メソッド

といった組み合わせが Method オブジェクトとして保持され、
call したタイミングで呼び出されています。
(そして call の第一引数が、保持されたメソッドの第一引数に渡されています)

そこでさっき書いた私が遭遇したコードを振り返ってみると、、、

class Hoge < ApplicationRecord
  # ローカル変数はそれぞれ初期化処理の中で定義されているものとします
  def hoge
    @hoge = @array.map(&method(:fuga))
  end

  def fuga(arg)
    # 引数 arg を使った何かしらの処理
  end
end

はい。Ruby 初心者の私はここで更に躓きました。

  • method メソッドに、呼び出し元がないじゃん (irb の例での h にあたるもの)
  • なんで、Object クラスの method メソッドが使えるのか

自己解決

  • method メソッドに、呼び出し元がないじゃん
    => メソッドの中で他のメソッドを呼び出すとき self を省略できるためでした。
    つまり、上記の method(:fuga) は、self.method(:fuga) となり、
    method メソッドを呼び出したのは、Hoge クラスから new されたインスタンスになります。

  • なんで、Object クラスの method メソッドが使えるのか
    => 基本 Ruby のクラスは Object クラスを継承しているためでした。

    irb(main):037* class Hoge
    irb(main):038> end
    =>  nil
    irb(main):039> Hoge.superclass
    => Object
    

ということで、先程の遭遇したコードにおける method(:fuga) では、

  • 呼び出し元:Hoge.new で作成されたインスタンス(h とします)
  • メソッド:fuga メソッド

ということになります。
正確ではないと思いますが、イメージしやすくすると
{ |arg| h.fuga(arg) } みたいなブロックを保持している状態です。
(fuga メソッドが引数を一つとるので、|arg| で引数を一つとるブロックになっています)

& の演算子について

引数の中で、& 演算子を Proc オブジェクトの前につけると、
ブロックとして引数に渡せるようになります。
(Proc オブジェクトについては、割愛します)

proc = Proc.new { |n| n * 2 }
[1, 2, 3 ].map(&proc)
=> [2, 4, 6]

つまり以下のように書いているのと同じです。

[1 ,2, 3].map { |n| n * 2 }

上記のように
「Proc オブジェクトをブロックとして引数に渡せるようになる」
とは別にもう一つ & 演算子には役割があります。
それは、「演算対象の to_proc メソッドを呼び出す」といった役割です。

to_proc メソッドは、Proc オブジェクトに対しては、
同じ Proc オブジェクトを返すだけですが、
Method.to_proc で Proc オブジェクトに変換する枠割があります。

Method オブジェクトから変換された Proc オブジェクトでも call によって
保持した 呼び出し元メソッド の組み合わせを呼び出すことができます。

なので、&method(:fuga) で、

  • 呼び出し元: self (定義しているクラスから作成されたインスタンス)
  • メソッド:fuga
    の組み合わせの Method オブジェクトから Proc オブジェクトへ変換し、
    map メソッドのブロック引数として渡していることになります。

map メソッドの動きについて

ここまで来たらもうイメージついているかもですが、最後にあらためて map メソッドについて考えてみます。

Array#map の動きを自分なりに言語化すると、
「与えられたブロック引数を Proc オブジェクトへ変換し、
各要素を第一引数として call を実行した結果を新しい配列の要素にする」
となるかと思います。

irb(main):017> p = Proc.new {|n| n * 2 }
=> #<Proc:0x0000ffff8f2312e0 (irb):17>
irb(main):019> [1, 2, 3].map(&p) # &p でブロック引数へ変換している
=> [2, 4, 6]

# それぞれの要素を第一引数に call を実施しているのと同じ
irb(main):020> p.call 1
=> 2
irb(main):021> p.call 2
=> 4
irb(main):022> p.call 3
=> 6

ということで、再度結論

map メソッドを呼んでいる配列(array)にの各要素を、メソッド名 symbol の第一引数に渡して実行し、その結果を要素にした配列を返してくれます。

@array.map(&method(:fuga)) は、
「@array の各要素を第一引数に渡して、Hoge クラスのインスタンス(self)の fuga メソッドを実行し、その結果を要素にした配列を返す。」
という結論に至りました。

Discussion