Rubyで配列などの要素を置換して、できない要素は捨てる

  • 4
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

配列などの要素を置き換えるだけならmapでいいのだが、ついでにうまく置き換えられない要素を削除したい場合がある。たとえばテキストファイルを一行ずつ見ていって、特定の正規表現にマッチする場合だけ抜き出すなど。

ストレートに書くならitems.grep(regexp).map{|i| regexp.match(i)}という感じになる。しかし複雑な処理になると、grep(あるいはselect, reject)とmapの中で同じようなコードが繰り返されることがある。そうなると一つのブロックで変換と削除をまとめて実行したくなる。

そのような場合、かつてはinjectがよく使われていたが、ブロックの実行結果が次の引数になる都合上、ArrayやHashのようなコンテナオブジェクトを作ろうとするとコードが冗長になることが多い。

items.inject([]) do |result, i|
  if check(i)
    result << convert(i)
  else
    result # これがないと条件が偽のときresultがnilになる
  end
end

with_objectを使うとより簡潔に書ける。

items.with_object([]) do |i, result|
  if check(i)
    result << convert(i)
  end
end

いくつか注意すべき点として:

  • injectとwith_objectではブロックの引数が逆。
    • with_indexなど、Enumerable/Enumeratorのメソッドチェーンで追加されるブロック引数は基本的に各要素の後になる。
    • たとえばitems.each.with_index(idx).with_object(obj).chunk(state){|((要素, with_indexのインデックス), with_objectのobj), chunkのstate|}という順番になる。
    • injectだけ逆と覚えればいいと思う。
  • with_objectにはimmutableなオブジェクトは使えない。そのときにはおとなしくinjectを使う。
    • each_with_object (Enumerable) - APIdockのNoteを参照(Railsのドキュメントだが、Ruby coreのwith_objectにも当てはまる)。
    • (0..3).each_with_object([0]){|i, o| o[0] += i}など、mutableなオブジェクトで包めばできないことはないが、だいたいinjectを使ったほうがシンプルに書ける。

そのほかに、条件文は中身が実行されなかったら値がnilになるという性質を利用したmapとcompactの合わせ技もときどき見る。条件文の中身の結果がnilになるときには使えないし、compactはArrayにしかないのでEnumerator全般で使いたい場合にはreject(&:nil?)などに置き換える必要があるが、.with_objectよりも.map.compactのほうがブロック内に現れる要素が少ないので読みやすいという人もいる[誰?]

あまり凝ったことをせずに、eachでベタッと書いたほうが分かりやすいかもしれない。


This document is licensed under CC0.