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

# 条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへ
## <i class="fa fa-cubes" style="font-size:1em;"></i> 概要
条件分岐とループベースのロジックからコレクションパイプラインを利用したロジックへの
置き換えについて説明します。

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

## <i class="fa fa-user" style="font-size:1em;"></i> この記事の対象
コレクションパイプラインによるロジックを扱ったことがない方・不慣れな方が対象です。

## <i class="fa fa-history" style="font-size:1em;"></i> コレクション操作の変遷
コレクションに対する処理はどの言語、どの領域のプログラミングでも頻出の課題です。

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

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

## <i class="fa fa-list-ol" style="font-size:1em;"></i> コレクション操作の基本：select / map ( collect ) / reduce ( inject )
コレクションパイプラインのサンプルの前に、
コレクション操作の基本である select / map ( collect ) / reduce ( inject )
について、ループ・条件分岐のロジックと対比しながら説明します。

### select
#### イメージ
![select.png](https://qiita-image-store.s3.amazonaws.com/0/43480/ea81e144-6fd7-8a1c-f03f-febee75aeef6.png)
#### 概要
コレクションから任意の条件を満たす要素を抽出します。
ここでは、 1 から 10 の数値リストから偶数の要素のみを抽出します。

#### コード
* 条件分岐とループ  

~~~ruby
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 メソッド

~~~ruby
list = [*1..10]
print list.select { |e|e.even? } # => [2, 4, 6, 8, 10]
~~~

* select メソッド ( Symbol#to_proc を利用した省略記法)

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

to_proc については下記を参照  
<i class="fa fa-external-link" style="font-size:1em;"></i> [るりま メソッド呼び出し(super・ブロック付き・yield)](http://docs.ruby-lang.org/ja/2.1.0/doc/spec=2fcall.html#block)

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

~~~ruby
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](https://qiita-image-store.s3.amazonaws.com/0/43480/79156708-481a-c367-c40a-14b8f90c2ad2.png)
#### 概要
コレクションの各要素に任意の処理を適用した結果を返却します。
ここでは、 1 から 10 の数値リストをすべて 2 倍にします。

Ruby では map と collect の二つのシンタックスシュガーが用意されています。
好みに合わせて使ってください。
詳しくは、<i class="fa fa-external-link" style="font-size:1em;"></i>  [るりま | map と collect の発想の違い](http://magazine.rubyist.net/?0038-MapAndCollect#l6) を参照。

#### コード
* 条件分岐とループ  

~~~ruby
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 メソッド

~~~ruby
list = [*1..10]
print list.map { |e|e * 2 } # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
~~~

### reduce ( inject )
#### イメージ
![reduce.png](https://qiita-image-store.s3.amazonaws.com/0/43480/deeca46d-9cfc-5510-25f0-8122c0a99dac.png)
#### 概要
コレクションの各要素に任意の処理を適用した結果を1つの変数に設定します。
ここでは、 1 から 10 の数値リストを合計にします。

Ruby では reduce と inject の二つのシンタックスシュガーが用意されています。
好みに合わせて使ってください。
<i class="fa fa-external-link" style="font-size:1em;"></i> [るりま | reduce と inject の発想の違い](http://magazine.rubyist.net/?0038-MapAndCollect#l9)

#### コード
* 条件分岐とループ  

~~~ruby
list = [*1..10]
total = 0

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

print total # => 55
~~~

* reduce メソッド

~~~ruby
list = [*1..10]
print list.reduce(&:+) # => 55
~~~

## <i class="fa fa-leaf" style="font-size:1em;"></i> はしやすめ
### tap
#### イメージ
![tap.png](https://qiita-image-store.s3.amazonaws.com/0/43480/febe865f-f28d-7838-2e00-36f9f71b5e4b.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
~~~

## <i class="fa fa-flask" style="font-size:1em;"></i> コレクションパイプライン サンプルケース
ここからが本番です。
Engineer クラスのコレクションを操作します。
Engineer クラスは以下の属性を持ちます。

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

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

~~~ruby: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
~~~

### <i class="fa fa-tint" style="font-size:1em;"></i> サンプル1: ホワイト企業の技術者の名前と年齢のリスト

~~~ruby
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]]
~~~

### <i class="fa fa-tint" style="font-size:1em;"></i> サンプル2: 企業名ごとにグループ化された技術者の名前と年齢のリスト

* group_by については <i class="fa fa-external-link-square" style="font-size:1em;"></i> [るりま | Enumerable#group_by](http://docs.ruby-lang.org/ja/2.1.0/method/Enumerable/i/group_by.html) をご確認ください

~~~ruby
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]]}]
~~~

### <i class="fa fa-tint" style="font-size:1em;"></i> サンプル3: 企業ごとの労働時間合計を降順でソート

* sort_by については <i class="fa fa-external-link-square" style="font-size:1em;"></i> [るりま | Enumerable#sort_by](http://docs.ruby-lang.org/ja/2.1.0/method/Enumerable/i/sort_by.html) をご確認ください

~~~ruby
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}]
~~~

### <i class="fa fa-tint" style="font-size:1em;"></i> サンプル4: 企業名ごとにグループ化された35歳より上の技術者の人数

~~~ruby
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}]
~~~

### <i class="fa fa-tint" style="font-size:1em;"></i> サンプル5: 燃え尽きた技術者からランダムで2名抽出し、名前の昇順でソート

* sample については <i class="fa fa-external-link-square" style="font-size:1em;"></i> [るりま | Array#sample](http://docs.ruby-lang.org/ja/2.1.0/method/Array/i/sample.html) をご確認ください

~~~ruby
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>]
~~~

## <i class="fa fa-arrow-circle-right" style="font-size:1em;"></i> 参照資料
<i class="fa fa-external-link" style="font-size:1em;"></i> [martinfowler.com | Collection Pipeline](http://martinfowler.com/articles/collection-pipeline/)