Enumerableモジュールを使いこなすことで、今まで書いてきたコードをより簡潔に書く事ができるかもしれない。
Enumerableモジュールにはmapやselectなど強力なメソッドが含まれているが、ここではreduceメソッドについて説明する。
1 2 |
irb(main):002:0> (1..5).inject(0) {|sum,n| sum + n } => 15 |
reduceメソッドのついて理解するためには以下の3つの理解が必要になる。
・Enumerableオブジェクト
→reduceメソッドのレシーバ。Enumerableモジュールをincludeしていないとreduceメソッドを呼び出すことができない。
・ブロック
→コレクションの各要素について1度ずつ実行される。eachメソッドのブロックと似ているが、reduceのブロックは戻り値を返す必要がある点で異なる。
・叩き込み結果の初期値(アキュムレータ)
→すべての要素がアキュムレータに畳み込まれると、その最終的な値がreduceの戻り値になる。
以下のような実装を考えてみる。
1 2 3 4 5 6 7 8 9 10 |
def sum(enum) enum.reduce(0) do |accumulater, element| accumulater + element end end # 実行 sum([1,2,3]) # 6 sum([1,2,3,4,5]) # 15 |
reduceの引数の0はアキュムレータの初期値である。ブロックの(accumulater + element)は次のブロックのアキュムレータに渡す、新しいアキュムレータを計算するために存在する。この例では、要素の値(element)を加算して、その結果を次のブロックのアキュムレータに渡している。この動作をコレクションの数だけ実行し、最終的なaccumulaterが reduceの戻り値となる。
reduceは引数の省略が可能で、省略した場合は、コレクションの先頭の要素が初期値となり、第二要素からイテレーションを開始する動作とある。意図しない結果を招く可能性があるので、基本的には初期値を設定しておくのが好ましい。
続いて以下のようなarrayオブジェクトをハッシュオブジェクトに変換する処理を考える。
1 2 3 4 5 6 7 8 9 |
array = [1,2,3] Hash[array.map{|x| [x, true]}] # {1=>true, 2=>true, 3=>true} # このような処理はreduceを使って以下のように書くことができる。 array.reduce({}) do |hash, element| hash.merge(element => true) end # {1=>true, 2=>true, 3=>true} |
初期値が空ハッシュ({})で、各ブロックの戻り値はハッシュ(mergeされた)を返すので最終的に求めるハッシュオブジェクトを得られる。
最後の例はあるユーザの配列をフィルタリングして、未成年のユーザを取り除いた(20歳以上のユーザのみ残す)ユーザ名の一覧を求める処理を考えてみる。
1 2 3 4 5 6 7 8 |
users.select{ |u| u.age >= 20 }.map(&:name) selectでusers配列を反復処理を実行した後にmapを使っているので、効率の観点だと以下のようにreduceを使ったほうが効率が良い。 users.reduce([]) do |names, user| names << user.name if user.age >= 20 names end |
まとめ
・reduceで書き換えられないか検討する
・アキュムレータの初期値は基本的には設定しておいた方が良い
・ブロックでは必ずアキュムレータを返すようにする