背景
- コードリーディング中に
each_with_objectというメソッドを見かけることが多い - すごく便利なのはわかってるんだけど、毎回何これ?ってなるのでまとめといた
each_with_object とは
Enumerable#each_with_object は、Ruby 1.9 で追加された組み込みメソッド。「初期値となるオブジェクト(ハッシュや配列)を用意して、ループしながら中に詰めていき、最後にそのオブジェクトを返す」という一連の処理を1メソッドで行える
基本構文
collection.each_with_object(初期オブジェクト) do |要素, オブジェクト|
# オブジェクトに要素を詰める処理
end
# → 完成したオブジェクトが戻り値になる
- 第1引数の
初期オブジェクトが、ブロックの第2引数オブジェクトとして毎回渡される - ブロック内で
オブジェクトを破壊的に変更していく - ループ終了後、完成した
オブジェクトが自動的に戻り値になる
each との比較
each で書く場合(3ステップ必要)
hash = {} # 1. 箱を用意
fruits.each do |fruit|
hash[fruit] = fruit.length # 2. ループで詰める
end
hash # 3. 箱を返す
変数の初期化・ループ処理・戻り値の返却が分散しており、特にメソッドの途中に書くと「この hash はどこで使われるんだ?」と読みづらくなる
each_with_object で書く場合(1ステップに集約)
fruits.each_with_object({}) do |fruit, hash|
hash[fruit] = fruit.length
end
初期化から戻り値まで1つの式にまとまるため、意図が明確になる。一時変数の宣言も不要
ループの流れを1回ずつ追う
理解のために、1ループずつ状態を書き出してみる
fruits = ["りんご", "みかん", "ぶどう"]
fruits.each_with_object({}) do |fruit, hash|
hash[fruit] = fruit.length
end
開始前: hash = {}
1回目: fruit="りんご" → hash["りんご"]=3 → {"りんご"=>3}
2回目: fruit="みかん" → hash["みかん"]=3 → {"りんご"=>3, "みかん"=>3}
3回目: fruit="ぶどう" → hash["ぶどう"]=3 → {"りんご"=>3, "みかん"=>3, "ぶどう"=>3}
終了: 完成したhashが戻り値として返る
ポイントは hash が毎回同じオブジェクトであること。fruit だけがループごとに変わり、hash には前回までの結果が蓄積されていく
実践例:URLからIDを抽出してハッシュを構築する
実際のRailsプロジェクトで使われていたパターン(プロジェクト名等はサンプルに差し替え)
projects.each_with_object({}) do |project, hash|
id = URI.parse(project.url).path.split('/').last
next unless id.match?(/\A\d+\z/)
hash[id.to_i] = project
end
結果のイメージ:
{ 123 => <Project A>, 456 => <Project B>, 789 => <Project C> }
このハッシュがあれば、IDで一発検索できる:
hash[456] # → <Project B>
コード中で使われているメソッド
-
URI.parse(url).path— URLからパス部分(例:/projects/456)を取り出す。URIは Ruby 標準ライブラリ -
split('/')— 文字列を指定した区切り文字で分割して配列にする。String#split -
last— 配列の最後の要素を取得する。Array#last -
match?(/\A\d+\z/)— 正規表現で文字列全体が数字のみかをチェックする。\Aは文字列の先頭、\d+は1文字以上の数字、\zは文字列の末尾。String#match?はマッチすればtrueを返す
感想
- 便利なんだけど、毎回忘れるんだよなぁ
参考
- Enumerable#each_with_object - Ruby リファレンス — 公式リファレンス