delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

Rubyで配列関数に&とシンボルを渡す記法の意味を理解する

Railsを書いていたら、アクティブレコードのTagモデルのリストをループしてname属性をカンマ区切りで表示したい場面がありました。

AIに聞いたところ、以下のように書けるとのことでした。この書き方の&:nameの部分を理解したいと思います。

tags.map(&:name).join(", ")

ミニチュア版のコードを用意

まずは以下のようにRailsに依存しないミニチュア版のコードを用意します。

index.rb

class Tag
  attr_reader :id, :name

  def initialize(id, name)
    @id = id
    @name = name
  end
end

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
puts tags.map(&:name).join(", ")

このコードが期待通り動作することを確認します。

実行結果

$ ruby index.rb
タグ1, タグ2

素直な書き方

Rubyのmap関数は以下の仕様です。

  • ブロックを受け取る
  • 各要素に対してブロックを評価した結果を全て含む配列を返す

docs.ruby-lang.org

従って、素直な書き方は以下のようになります。

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
tag_names = tags.map do |tag|
  tag.name
end
puts tag_names.join(", ") # タグ1, タグ2

tagsからtag_namesを導出するところを1行にして以下のようにも書けます。

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
tag_names = tags.map {|tag| tag.name}
puts tag_names.join(", ") # タグ1, タグ2

Procを理解する

ここから先に進むにはProcというものを理解する必要があります。

公式ドキュメントには「ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。」とあります。

docs.ruby-lang.org

JSが分かる人は関数オブジェクトのようなものと理解するとよいようです。(私もJS経験者)

一番シンプルなProcの例は以下のようなコードです。

hello_proc = Proc.new {|name| puts "Hello #{name}!"}
hello_proc.call("山田")

実行結果

$ ruby index.rb
Hello 山田!

Procオブジェクトをmap関数に渡す

次にProcオブジェクトをmap関数に渡してみます。

先のサンプルコードでmap関数のブロックに記述していたコードをProc化します。

tag_name_proc = Proc.new {|tag| tag.name }

Procオブジェクトをブロックとしてmap関数に渡します。Rubyの記法として先頭に&をつけます。

tag_names = tags.map(&tag_name_proc)

実行部分のコード全体は以下のようになります。

tags = [Tag.new(1, "タグ1"), Tag.new(2, "タグ2")]
tag_name_proc = Proc.new {|tag| tag.name }
tag_names = tags.map(&tag_name_proc)
puts tag_names.join(", ")

実行結果

$ ruby index.rb
タグ1, タグ2

シンボルのto_procメソッド

Rubyのシンボルはto_procメソッドを持っています。

docs.ruby-lang.org

to_procメソッドが返すProcオブジェクトは、callの第一引数に渡した変数をレシーバーとしてメソッドを実行します。

tag = Tag.new(1, "タグ1")
tag_name_proc = :name.to_proc
puts tag_name_proc.call(tag) # タグ1

ちなみに存在しないメソッド名を使うとエラーになりました。

tag = Tag.new(1, "タグ1")
tag_priority_proc = :priority.to_proc
puts tag_priority_proc.call(tag)

実行結果

$ ruby index.rb
index6.rb:12:in '<main>': undefined method 'priority' for an instance of Tag (NoMethodError)

最後に、Rubyではメソッドに&とともにシンボルを渡すと以下の挙動になるようです。(公式ドキュメントより引用)

  1. to_procが呼ばれてProcオブジェクト化される
  2. 1.がブロックとして渡される

以上より、2つは同じ意味になります。

素直なコード

tag_names = tags.map {|tag| tag.name}

シンボルとto_procを使った省略記法

tag_names = tags.map(&:name).join(", ")

参考文献

『プロを目指す人のためのRuby入門[改訂2版⁠]⁠ 言語仕様からテスト駆動開発デバッグ技法まで』の 「10.5.2 &とto_procメソッド」を参考にさせていただきました。(P431〜p432)

gihyo.jp