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関数は以下の仕様です。
- ブロックを受け取る
- 各要素に対してブロックを評価した結果を全て含む配列を返す
従って、素直な書き方は以下のようになります。
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というものを理解する必要があります。
公式ドキュメントには「ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。」とあります。
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メソッドを持っています。
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ではメソッドに&とともにシンボルを渡すと以下の挙動になるようです。(公式ドキュメントより引用)
to_procが呼ばれてProcオブジェクト化される- 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)