【Ruby】map/filter_map/flat_mapの概要と使い方

Ruby

map(collect)について

mapは各要素を変換して新たな配列を作成するメソッドです。collectmapのエイリアス(等価のメソッド)です。
オブジェクトのプロパティや配列の要素から新たな配列を作成したい場合にmapが活躍します。

> values = [1, 2, 3]

### 各要素を加工して新たな配列を作成
> values.map { |value| value * 2 }
=> [2, 4, 6]

mapを利用することでオブジェクトを要素にもつ配列も作成できます。具体例は以下の通りです。

> user_names = ["user_1", "user_2", "user_3"]

> users = user_names.map { |name| { name: name } }
=> [{:name=>"user_1"}, {:name=>"user_2"}, {:name=>"user_3"}]

### Arrayの要素としてオブジェクトがセットされている
> users[0][:name]
=> "user_1"

『&:』による省略記法について

map { |x| x.メソッド名 }あるいはmap { |x| x.属性名 }&:を利用した省略記法ができます。具体例は以下の通りです。

> values = [1, 2, 3]

### &:による省略記法
> values.map(&:to_s)
=> ["1", "2", "3"]

### 省略しない場合
> values.map { |value| value.to_s }
=> ["1", "2", "3"]

&:を利用することで例えば以下のようにモデルの属性参照が簡潔に記述できます。

users = []
3.times do  |i|
  users << User.new(name: "user_#{i + 1}")
end

> users.map(&:name)
=> ["user_1", "user_2", "user_3"]

ハッシュに対してmap(collect)を実行する方法

Hashに対してmapを実行する場合はブロックで利用する変数を2つ用意します。
例えばmap { |key, value| }とすることでキー名がkey、値がvalueの変数にセットされます。具体例は以下の通りです。

> fruite_prices = { banana: 100, orange: 130, apple: 100 }

> fruites = fruite_prices.map { |key, value| { name: key.to_s, price: value } }
=> [{:name=>"banana", :price=>100}, {:name=>"orange", :price=>130}, {:name=>"apple", :price=>100}]

mapの戻り値はArrayなので注意

ハッシュに対してmapを実行した場合もmapはハッシュではなくArrayを返します。
mapの結果をハッシュに変換するにはto_hあるいはHash[]を利用します。具体例は以下の通りです。

> fruite_prices = { banana: 100, orange: 130, apple: 100 }

### mapの結果はArray
> fruite_prices_with_tax = fruite_prices.map { |key, value| [key, (value * 1.1).to_i] }
=> [[:banana, 110], [:orange, 143], [:apple, 110]]

### to_hを利用してハッシュに変換
> fruite_prices_with_tax.to_h
=> {:banana=>110, :orange=>143, :apple=>110}

### Hash[]を利用してハッシュに変換
> Hash[fruite_prices_with_tax]
=> {:banana=>110, :orange=>143, :apple=>110}

要素の値のみを変換したい場合はtransform_valueを利用するとよい

ハッシュの各要素の値のみを変換したい場合はmapではなくtransform_valueを利用するとよいです。具体例は以下の通りです。

> fruite_prices = { banana: 100, orange: 130, apple: 100}
=> {:banana=>100, :orange=>130, :apple=>100}

> fruite_prices_with_tax = fruite_prices.transform_values { |value| (value * 1.1).to_i }
=> {:banana=>110, :orange=>143, :apple=>110}

特定の条件を満たした要素のみmapする場合

今回は2つの方法を紹介します。

特定の条件を満たした要素のみmapする方法
  • if文 + compact
  • filter(select)してからmap

if文 + compact

mapのブロック内に条件文を追加して要素をフィルタリングする方法です。mapの結果にnilの要素が含まれるためcompactnilを除外します。具体例は以下の通りです。

users = []
3.times do  |i|
  users << User.new(name: "user_#{i + 1}", is_active: "#{i.even?}")
end

### is_activeがtrueなnameを取り出す
> users.map { |user| user.name if user.is_active? }.compact
=> ["user_1", "user_3"]

## 参考: compactを利用しない場合
> users.map { |user| user.name if user.is_active? }
=> ["user_1", nil, "user_3"]

filter(select)してからmap

filterを利用することで条件を満たさない要素を配列から取り除けます。selectfilterのエイリアス(等価のメソッド)です。

filterの例は以下の通りです。

> values = [1, 2, 3]
> values.filter { |value| value if value.even? }
=> [2]

filtermapを組み合わせることで特定の条件を満たした要素をmapできます。具体例は以下の通りです。

users = []
3.times do  |i|
  users << User.new(name: "user_#{i + 1}", is_active: "#{i.even?}")
end

> users.filter { |user| user.is_active? }.map(&:name)
=> ["user_1", "user_3"]

なお、主キーのように要素が一意に定まる条件で検索をする場合はfilterの代わりにfindを利用するとよいです。
filterfindの違いの詳細解説についてはRubyのfilter(select)とfind(detect)の違いで紹介しています。

filter_mapは『filter + map』を一括で実行するメソッド

filter_mapを利用するとfiltermapを一括で実行できます。つまり、特定の条件を満たす要素にのみmapを実行するメソッドがfilter_mapです。filter_mapの具体例は以下の通りです。

> (1..10).filter_map { |i| i * 2 if i.even? }
=> [4, 8, 12, 16, 20]

filter + map』をfilter_mapで書き換えた例は以下の通りです。

users = []
3.times do  |i|
  users << User.new(name: "user_#{i + 1}", is_active: "#{i.even?}")
end

### filter + mapで実装した場合
> users.filter { |user| user.is_active? }.map(&:name)
=> ["user_1", "user_3"]

### filter_mapで書き換えた場合
> users.filter_map { |user| user.name if user.is_active? }

flat_map(collect_concat)は『map + flatten』を一括で実行するメソッド

flat_mapを利用するとmapflattenを一括で実行できます。flattenとは多次元配列を1次元配列にするメソッドです。
collect_concatflat_mapのエイリアス(等価のメソッド)です。

map + flatten』をflat_mapに書き換えた例は以下の通りです。

> values = [1, 3, 5]

### map + flattenで実装した場合
> values.map { |value| (value..(value + 1)).to_a }.flatten
=> [1, 2, 3, 4, 5, 6]

### flat_mapで書き換えた場合
> values.flat_map { |value| (value..(value + 1)).to_a }
=> [1, 2, 3, 4, 5, 6]

### 参考: mapのみを利用した場合の結果
> values.map { |value| (value..(value + 1)).to_a }
=> [[1, 2], [3, 4], [5, 6]]

flat_mapのflattenはflatten(1)相当なので注意

flattenが多次元配列を1次元配列に変換するのに対し、flat_mapはn次元配列を(n-1)次元配列にします。つまり、flat_mapによる次元削除はflatten(1)(配列を1次元減らす)に相当するので注意が必要です。

多次元配列を確実に1次元に変換したい場合はflat_mapではなくflattenを利用するとよいです。

以下にflat_mapflattenの次元削除の例を紹介します。

### mapを利用して3次元配列になる例
> values = [1, 2, 3]
> values.map { |value| [[value]] }
=> [[[1]], [[2]], [[3]]]

### flat_mapだと2次元配列になる
> values.flat_map { |value| [[value]] }
=> [[1], [2], [3]]

### flat_mapのflattenはflatten(1)と等価
> values.map { |value| [[value]] }.flatten(1)
=> [[1], [2], [3]]

### flattenだと1次元配列になる
> values.map { |value| [[value]] }.flatten
=> [1, 2, 3]

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考記事