知りたかったこと
filterは配列などの条件に合致する要素を新たな配列として返してくれる。例えば次は機能する。
ary = [1,10,6,3,7,2,5,4]
p ary.filter { _1 > 4 }
#=> [10,6,7,5]
しかし、以下の場合はどうするだろうか。
- 配列の合致する要素に対してのみ、その要素に演算を行なった値を返して欲しい
- 配列の合致する要素のインデックスが全て欲しい
要は、Pythonのif節を備えた内包表記と同じことがしたいのである。
いにしえの解決法
これを解決するには
map{if文}.compactを使いましょう、というのが調べると出てくる。要らない要素はif文でnilにし、最後にcompactで取り除くという算段だ。
ary = [1,10,6,3,7,2,5,4]
p ary.map { _1 * 2 if _1 > 4 }.compact
#=> [20,12,14,10]
p ary.map.with_index {|value, index| index if value > 4 }.compact
#=> [1,2,4,6]
ちなみに、each_with_indexはあってもmap_with_indexはない。大人しくmap.with_indexとする。
もっと良い案
しかし、フィルタリングするために用いるメソッドがcompactというのはいかにも分かりづらく、技として理解していても可読性が低い。
一案として、filterとmapを分けてあげる手がある。
ary.filter { _1 > 4 } .map { _1 * 2 }
#=> [20, 12, 14, 10]
これでもいいのだけど、ブロックが二つ必要になったりとpythonの内包表記に一歩及ばない感じもする。
フィルタリングとマッピングを同時にしてくれるメソッドは無いものか、と思ったら、ちゃんとあった。
filter_map (Ruby公式リファレンスに飛びます)
先ほどのはこうなる。filter{}.map{}とどちらが良いかは時と場合によるだろうが、見やすく感じる。
ary = [1,10,6,3,7,2,5,4]
p ary.filter_map { _1 * 2 if _1 > 4 }
#=> [20,12,14,10]
特に、with_indexでインデックスを返り値に利用したいときにはfilter_map一択になると思う。
if文にindexが入っていようが、問題なく動く。
ary = [1,10,6,3,7,2,5,4]
p ary.filter_map.with_index {|value, index| index if value > 4 }
#=> [1,2,4,6]
p ary.filter_map.with_index {|value, index| value if index >= value }
#=> [3,2,5,4]
良いじゃん。
こんな素敵なメソッドなのにいまいち知名度が低い気がするのは、実装がRuby2.7以降とまあまあ後発だからか。
また、filterのエイリアスでselectはあってもselect_mapは存在しない(要注意)ため、selectを好む人は存在に気づきづらいかもしれない。
ついでなのでハッシュを生成するto_hとの合わせ技もおさらいしておこう。to_hは[キー,値]という配列を要素に持つ二次元配列からハッシュを生成出来るのだった。
[[:a, 1], [:b, 2], [:c, 3]].to_h
#=> {:a=>1, :b=>2, :c=>3}
これとfilter_mapを組み合わせることももちろんできる。
ary = [1,10,6,3,7,2,5,4]
p ary.filter_map.with_index {|val, ind| [ind, val * 2] if val > 4 }.to_h
#=> {1=>20, 2=>12, 4=>14, 6=>10}
もはやfilterメソッドは不要なんじゃないかと錯覚させられるくらいに小気味がいい。
zipしたっていい。
ary1 = [1,6,3,8]
ary2 = [5,2,7,4]
p ary1.zip(ary2).filter_map {|i, j| [i, j] if i > j}.to_h
#=> {6=>2, 8=>4}
map, map.with_index, filter_map, filter_map.with_index, to_hの5つの挙動を理解すれば、少なくともPythonの内包表記と同等かそれ以上の機動性を実現可能で、望みの配列やハッシュは大概自由に作れるようになる。と思う。
おまけ
injectの場合、filter_injectなるものは存在しない。おとなしくfilterしてからinjectする。
ary = [1,10,6,3,7,2,5,4]
ary.filter{ _1 > 4}.inject(:+)
#=> 28