MAGAZINE
ルーターマガジン
RubyのEnumerableでメソッドチェーンするときのコツ
こんにちは。アルバイトの近藤です。
みなさんはRubyのイテレーター、回していますでしょうか。 map, zip, inject, group_by, ..... 一通りメソッドを覚えると、この処理にはコレがぴったり、みたいなものが徐々に見えてくるかと思います。
ですが、これらのメソッドをチェーンしたとき、挙動を正しくイメージできますか?
selectやmap, cycleなどのチェーンはこんな感じで、イメージしやすいかと思います。
[1, 100, 10000].select { |x| x > 10 }.cycle(3).map { |x| x * 2 }
#=> [200, 20000, 200, 20000, 200, 20000]
では、each_with_indexとinjectをチェーンすると...? zipにeach_with_indexを繋げたときの構造は...?
今回は、Enumerableモジュールのメソッドチェーンのコツについて紹介します。
難しいのはなぜ?
そもそも、mapやselectのチェーンは書けるのに、each_with_indexやinject, zipを繋げると難しくなるのはなぜでしょうか? 原因は2つあります。
ひとつは メソッドが返す配列やEnumeratorの形が複雑 であること、 もうひとつは メソッドが受け取るブロックの引数が複数ある こと、です。
これらの壁を乗り越えればメソッドチェーンは怖くありません。 具体例と共に見ていきましょう。
① メソッドが返す配列やEnumeratorの形が複雑
例に挙げたmapやselect, cycleは、返り値が一次元の配列です (cycleの返り値は一次元の配列のようなEnumeratorですが、メソッドチェーンする際は配列と同じように考えてよいです)。 変わるのは列の中身や長さだけなので、構造を気にせずチェーンできていたわけです。
そうではなく難しいのは、返り値の構造が複雑な場合です。
配列が入れ子になって返ってくるメソッド
- chunk
[[結果, 要素], ...]
- slice_after
[[要素, ..], [要素, .....], ...]
- each_cons
[[要素, ...], [要素, ...], ...]
- zip
[[要素, ...], [要素, ...], ...]
- each_with_index
[[要素, 0], [要素, 1], ...]
- chunk_while
[[要素, ...], [要素, ...], ...]
ハッシュが返ってくるメソッド
これらをメソッドチェーンをするときは、返り値の構造の把握が大事です。
たとえば.map{|x| ... }
と繋げるとき、配列がどんなに複雑であろうと、x
に入るのは一番外側の配列の要素ひとつです。
そのくらいわかるよと思われるかもしれませんが、この後each_with_indexやinjectを繋げるときに(後述します)この意識が効いてくるのです。
['壱','弐','参'].zip(['Ⅰ', 'Ⅱ', 'Ⅲ'], [1,2,3])
#=> [["壱", "Ⅰ", 1], ["弐", "Ⅱ", 2], ["参", "Ⅲ", 3]]
['壱','弐','参'].zip(['Ⅰ', 'Ⅱ', 'Ⅲ'], [1,2,3]).map{|x| "#{x[0]}の#{x[1]}の#{x[2]}" }
#=> ["壱のⅠの1", "弐のⅡの2", "参のⅢの3"]
['a','a','a','b','b','c'].tally.map{|x| "#{x[0]}は#{x[1]}個" }
#=> ["aは3個", "bは2個", "cは1個"]
ちなみにRubyには配列分解という、括弧を用いて引数を分解する記法があり、これを使うと便利です。
['壱','弐','参'].zip(['Ⅰ', 'Ⅱ', 'Ⅲ'], [1,2,3]).map { |(ja, en, n)| "#{ja}の#{en}の#{n}" }
#=> ["壱のⅠの1", "弐のⅡの2", "参のⅢの3"]
['a','a','a','b','b','c'].tally.map { |(value, num)| "#{value}は#{num}個" }
#=> ["aは3個", "bは2個", "cは1個"]
② メソッドが受け取るブロックの引数が複数ある
ブロックの引数が複数のメソッドとは、例えばeach_with_index, injectなどが該当します。 これらをシンプルな配列(やEnumerator)に対して使う分には問題ありません。ドキュメントに詳しい例も載っています。 難しいのは、複雑な構造に対して使うときです。 複雑な構造のレシーバがブロック引数のどこに対応するのか、これが鍵です。 このパターンは次の3種類の形式さえ押さえればOKです。見ていきましょう。
each_with_index
each_with_index {|item, index| ... }
each_with_indexでは、ブロックの1つ目の引数に要素が渡されます。 普通の配列内の要素も、複雑な配列内の配列も、常にブロック引数の1つ目です。
['零','壱','弐','参'].zip(['_', 'Ⅰ', 'Ⅱ', 'Ⅲ']).each_with_index{|(ja, en), n| puts "#{ja}の#{en}の#{n}" }
#=> 零の_の0
#=> 壱のⅠの1
#=> 弐のⅡの2
#=> 参のⅢの3
inject
inject {|result, item| ... }
injectではブロック引数の2つ目に要素が渡されます。 入れ子の配列であっても、2つ目の引数にその配列がまるごと渡されます。
① で紹介したeach_with_indexの返り値はこのような形でした。
each_with_index #=> [[要素, 0], [要素, 1], ...]
というわけで、冒頭で紹介したeach_with_indexとinjectのチェーンはこのようになります。
['零','壱','弐','参'].each_with_index.to_a
#=> [["零", 0], ["壱", 1], ["弐", 2], ["参", 3]]
['零','壱','弐','参'].each_with_index.inject(''){|memo, (item, index)| memo + "#{item}は#{index.even? ? '偶数' : '奇数'}. " }
#=> "零は偶数. 壱は奇数. 弐は偶数. 参は奇数. "
chunk_while
chunk_while {|elt_before, elt_after| ... }
慣れてきましたか? 最後、chunk_whileは1つ目と2つ目両方の引数に渡されます。
uno = [['red', 1], ['red', 4], ['blue', 4], ['green', 8], ['green', 1], ['yellow', 9]]
uno.chunk_while{|(c1, n1), (c2, n2)| c1 == c2 || n1 == n2}.to_a
#=> [
# [["red", 1], ["red", 4], ["blue", 4]],
# [["green", 8], ["green", 1]],
# [["yellow", 9]]
# ]
おわりに
繰り返しになりますが、大事なことはこの2つです。
- メソッドが返す配列の構造の把握
- レシーバの要素がブロック引数のどこに来るかを確認
これさえ理解してしまえばほとんどのメソッドチェーンには対応できるはずです。
以上、独自理論を展開しているところもありましたが、Enumerableのメソッドチェーンのコツについてでした。
CONTACT
お問い合わせ・ご依頼はこちらから