MAGAZINE

ルーターマガジン

Ruby

RubyのEnumerableでメソッドチェーンするときのコツ

2020.08.06
Pocket

こんにちは。アルバイトの近藤です。

みなさんは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ですが、メソッドチェーンする際は配列と同じように考えてよいです)。 変わるのは列の中身や長さだけなので、構造を気にせずチェーンできていたわけです。

そうではなく難しいのは、返り値の構造が複雑な場合です。

配列が入れ子になって返ってくるメソッド

ハッシュが返ってくるメソッド

  • group_by
    • {結果 => [要素, ...], ...}
  • tally
    • {要素 => 個数, ...}
    • Ruby2.7から使えます

これらをメソッドチェーンをするときは、返り値の構造の把握が大事です。 たとえば.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のメソッドチェーンのコツについてでした。

Pocket

CONTACT

お問い合わせ・ご依頼はこちらから