RubyDay 4

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

More than 3 years have 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