条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへ #ruby

  • 38
    Like
  • 2
    Comment
More than 1 year has passed since last update.

条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへ

概要

条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへの
置き換えについて説明します。

  • コレクションパイプラインに関する概要を説明
  • コレクション操作の基本: select / map ( collect ) / reduce ( inject )
  • はしやすめ
  • コレクションパイプライン サンプルケース

この記事の対象

コレクションパイプラインによるロジックを扱ったことがない方・不慣れな方が対象です。

コレクション操作の変遷

コレクションに対する処理はどの言語、どの領域のプログラミングでも頻出の課題です。

コレクションの操作は言語の基本的な文法としてサポートされている
条件分岐やループで処理することもできます。
コレクションパイプラインを利用すると、分かりやすく・スマートに記述することができます。

コレクションパイプラインは、ある処理の結果のコレクションを、
次の処理の入力とし、一連の連続した計算を行うパターンです。
そう、パイプを利用した UNIX のコマンドのように。

コレクション操作の基本:select / map ( collect ) / reduce ( inject )

コレクションパイプラインのサンプルの前に、
コレクション操作の基本である select / map ( collect ) / reduce ( inject )
について、ループ・条件分岐のロジックと対比しながら説明します。

select

イメージ

select.png

概要

コレクションから任意の条件を満たす要素を抽出します。
ここでは、 1 から 10 の数値リストから偶数の要素のみを抽出します。

コード

  • 条件分岐とループ
list = [*1..10]
even_list = []

for i in 0..(list.size - 1) do
  next if list[i].odd?
  even_list << list[i]
end

print even_list # => [2, 4, 6, 8, 10]
  • select メソッド
list = [*1..10]
print list.select { |e|e.even? } # => [2, 4, 6, 8, 10]
  • select メソッド ( Symbol#to_proc を利用した省略記法)

ブロックを受け取るメソッドにProc以外を渡すと、
自動的に to_proc が呼び出され、型変換されます。

to_proc については下記を参照

るりま メソッド呼び出し(super・ブロック付き・yield)

ブロックの記述が不要になり、ブロックの仮引数を書く必要もなくなりました。
(仮引数 = ひとつ前の例の |e| の部分)

list = [*1..10]
print list.select(&:even?) # => [2, 4, 6, 8, 10]
  • reject メソッド 条件を満たさない要素を抽出します。 select の反対です。 select 内に否定演算子を書かなければならないような場合は こちらを利用するほうが自然になります。
list = [*1..10]
print list.reject(&:odd?) # => [2, 4, 6, 8, 10]

map ( collect )

イメージ

map.png

概要

コレクションの各要素に任意の処理を適用した結果を返却します。
ここでは、 1 から 10 の数値リストをすべて 2 倍にします。

Ruby では map と collect の二つのシンタックスシュガーが用意されています。
好みに合わせて使ってください。
詳しくは、 るりま | map と collect の発想の違い を参照。

コード

  • 条件分岐とループ
list = [*1..10]
double_list = []

for i in 0..(list.size - 1) do
  double_list << list[i] * 2
end

print double_list # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
  • map メソッド
list = [*1..10]
print list.map { |e|e * 2 } # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

reduce ( inject )

イメージ

reduce.png

概要

コレクションの各要素に任意の処理を適用した結果を1つの変数に設定します。
ここでは、 1 から 10 の数値リストを合計にします。

Ruby では reduce と inject の二つのシンタックスシュガーが用意されています。
好みに合わせて使ってください。
るりま | reduce と inject の発想の違い

コード

  • 条件分岐とループ
list = [*1..10]
total = 0

for i in 0..(list.size - 1) do
  total += list[i]
end

print total # => 55
  • reduce メソッド
list = [*1..10]
print list.reduce(&:+) # => 55

はしやすめ

tap

イメージ

tap.png

概要

コレクション操作の途中で値を確認したい場合、 tap メソッドを重宝します。

コード

1-10 の数値配列から

  • 奇数のみを抽出
  • 2倍にする
  • 合計する

という操作をします。
2倍にした直後の値を確認するために、 tap を利用します。

list = [*1..10]
puts list.select(&:odd?)
          .map { |e|e * 2 }
          .tap { |e|print e, "\n" }
          .reduce(&:+)
  • 出力
[2, 6, 10, 14, 18]
50

コレクションパイプライン サンプルケース

ここからが本番です。
Engineer クラスのコレクションを操作します。
Engineer クラスは以下の属性を持ちます。

key type contents
name String 名前
age Fixnum 年齢
burn_out bool 燃え尽きたかどうか
company String 所属企業
hours_worked_per_annum Fixnum 月労時間

基礎データ

各サンプルから require して利用する共通データ。
以下の項目を持ちます。

engineers.rb
Engineer = Struct.new(:name, :age, :burn_out, :company, :hours_worked_per_annum)
def read_engineers
  engineers_src = [
    { name: 'tanaka', age: 23, burn_out: false, company: 'white', hours_worked_per_annum: 170 },
    { name: 'sato', age: 50, burn_out: false, company: 'white', hours_worked_per_annum: 160 },
    { name: 'honda', age: 32, burn_out: false, company: 'white', hours_worked_per_annum: 180 },
    { name: 'suzuki', age: 32, burn_out: false, company: 'normal', hours_worked_per_annum: 180 },
    { name: 'fujita', age: 32, burn_out: false, company: 'normal', hours_worked_per_annum: 180 },
    { name: 'kaneda', age: 32, burn_out: false, company: 'normal', hours_worked_per_annum: 180 },
    { name: 'nonomura', age: 48, burn_out: true, company: 'black', hours_worked_per_annum: 400 },
    { name: 'obokata', age: 31, burn_out: true, company: 'black', hours_worked_per_annum: 300 },
    { name: 'katayama-yuchan', age: 123, burn_out: true, company: 'black', hours_worked_per_annum: 350 },
    { name: 'samuragochi', age: 42, burn_out: false, company: 'black', hours_worked_per_annum: 50 }
  ]
  engineers_src.map do |e|
    Engineer.new(
      e[:name],
      e[:age],
      e[:burn_out],
      e[:company],
      e[:hours_worked_per_annum]
    )
  end
end

サンプル1: ホワイト企業の技術者の名前と年齢のリスト

require 'pp'
require './engineers'

engineers = read_engineers
pp engineers.select { |e|e.company == 'white' }
            .map { |e|[e.name, e.age] }
__END__
[["tanaka", 23], ["sato", 50], ["honda", 32]]

サンプル2: 企業名ごとにグループ化された技術者の名前と年齢のリスト

require 'pp'
require './engineers'

engineers = read_engineers
pp engineers.group_by { |e| e.company }
            .map { |k, v|{k => v.map { |e|[e.name, e.age] } } }
__END__
[{"white"=>[["tanaka", 23], ["sato", 50], ["honda", 32]]},
 {"normal"=>[["suzuki", 32], ["fujita", 32], ["kaneda", 32]]},
 {"black"=>
   [["nonomura", 48],
    ["obokata", 31],
    ["katayama-yuchan", 123],
    ["samuragochi", 42]]}]

サンプル3: 企業ごとの労働時間合計を降順でソート

require 'pp'
require './engineers'

engineers = read_engineers
pp engineers.group_by { |e| e.company }
            .map { |k, value|{ name: k, total_hours_worked_per_annum: value.reduce(0) { |a, e|a + e.hours_worked_per_annum } } }
            .sort_by { |e|-e[:total_hours_worked_per_annum] }
__END__
[{:name=>"black", :total_hours_worked_per_annum=>1100},
 {:name=>"normal", :total_hours_worked_per_annum=>540},
 {:name=>"white", :total_hours_worked_per_annum=>510}]

サンプル4: 企業名ごとにグループ化された35歳より上の技術者の人数

require 'pp'
require './engineers'

engineers = read_engineers
pp engineers.group_by { |e| e.company }
            .map { |k, v|{ k => v.count { |e|e.age >= 35 } } }
__END__
[{"white"=>1}, {"normal"=>0}, {"black"=>3}]

サンプル5: 燃え尽きた技術者からランダムで2名抽出し、名前の昇順でソート

require 'pp'
require './engineers'

engineers = read_engineers
pp engineers.select(&:burn_out)
            .sample(2)
            .sort_by(&:name)
__END__
実行 1 回目
[#<struct Engineer
  name="katayama-yuchan",
  age=123,
  burn_out=true,
  company="black",
  hours_worked_per_annum=350>,
 #<struct Engineer
  name="obokata",
  age=31,
  burn_out=true,
  company="black",
  hours_worked_per_annum=300>]

実行 2 回目
[#<struct Engineer
  name="nonomura",
  age=48,
  burn_out=true,
  company="black",
  hours_worked_per_annum=400>,
 #<struct Engineer
  name="obokata",
  age=31,
  burn_out=true,
  company="black",
  hours_worked_per_annum=300>]

参照資料

martinfowler.com | Collection Pipeline