こんにちは、とまだです。
Ruby アドベントカレンダー 1 日目の記事をお届けします!
みなさんは普段から Ruby を使っていると思いますが、each
やmap
以外の Enumerable メソッドってあまり使ったことないんじゃないでしょうか?
(自分も最初はeach
とmap
しか使ってなかったので、偉そうなことは言えませんが...)
実はEnumerable
モジュールには、めちゃくちゃ便利なメソッドがたくさん隠れています。
今回は、使える × 面白いの二軸で選んだメソッドをご紹介します!
1. 意外と演算子だったメソッドたち
まずは、数学っぽい演算をしてくれるメソッドを紹介します。
chain でコレクションを繋げる
よく、配列を結合したいときがありますよね。
そんなとき、chain
を使うと便利です。
[1, 2].chain([3, 4]) #=> Enumerator: [1, 2, 3, 4]
chain
は、複数のコレクションを繋げて一つのEnumerator
を返してくれます。
一方、chain
を使わない場合は、以下のように書くことができます。
# 配列の結合
[1, 2] + [3, 4]
# もしくは
[1, 2].concat([3, 4])
「あれ?+
やconcat
と同じじゃない?」と思った方、鋭いですね!
実はchain
はEnumerator
を返すので、必要になるまで実際の配列は作られません。
# chainを使うと
result = [1, 2].chain([3, 4])
# この時点では新しい配列は作られていない
# 実際に使うときに初めて配列が作られる
result.to_a #=> [1, 2, 3, 4]
言い換えると、chain
を使うとメモリ効率がよくなるということです。
大きなデータを扱うときには、chain
を使うといいかもしれませんね!
product で直積を取る
では次に、product
メソッドを紹介します。
[1, 2].product(['a', 'b'])
#=> [[1, "a"], [1, "b"], [2, "a"], [2, "b"]]
これは、組み合わせを全部作ってくれるメソッドです。
たとえば、商品の色とサイズの全組み合わせを作るときに使えます。
product
を使わない場合は、以下のように書くことができます。
colors = ['Red', 'Blue']
sizes = ['S', 'M', 'L']
combinations = []
colors.each do |color|
sizes.each do |size|
combinations << [color, size]
end
end
#=> [["Red", "S"], ["Red", "M"], ["Red", "L"], ["Blue", "S"], ["Blue", "M"], ["Blue", "L"]]
# もしくは
colors.flat_map {|color|
sizes.map {|size| [color, size]}
}
上記のように、each
やmap
を使っても同じ結果が得られますが、product
を使うともっと簡潔に書けます。
# 商品の色とサイズの全組み合わせを作る
colors = ['Red', 'Blue']
sizes = ['S', 'M', 'L']
colors.product(sizes)
同じ結果を得られるなら、短くすっきり書ける方がいいですね!
2. ゲーム開発で使えそうなメソッドたち
次は、ゲーム開発で使えそうなメソッドを紹介します。
repeated_permutation で同じ要素を使った順列
repeat_permutation
は、同じ要素を使って順列を作ってくれるメソッドです。
「順列」とは、要素の順番が重要な組み合わせのことです。
[1, 2, 3].repeated_permutation(2)
#=> [[1,1], [1,2], [1,3], [2,1], [2,2], [2,3], [3,1], [3,2], [3,3]]
たとえば、サイコロを 3 回振るパターンを全部列挙したいときに使えます。
普通なら、こうでしょうか。
dice = [1, 2, 3, 4, 5, 6]
patterns = []
dice.each do |d1|
dice.each do |d2|
dice.each do |d3|
patterns << [d1, d2, d3]
end
end
end
ネストが深くなってしまいますね。
これもrepeated_permutation
を使えば、一行で書けます。
[1, 2, 3, 4, 5, 6].repeated_permutation(3)
slice_when で連続する要素をグループ化
[1,1,2,2,2,3,4,4,4].slice_when {|a,b| a != b}
#=> [[1,1], [2,2,2], [3], [4,4,4]]
これは連続する同じ要素をグループ化してくれるメソッドです。
たとえば、マッチ 3 系のゲームで同じ色のブロックが何個連続しているか数えるときに使えます。
あえて slice_when
を使わない場合は、以下のように書くことができます。
numbers = [1,1,2,2,2,3,4,4,4]
result = []
current_group = [numbers.first]
numbers[1..-1].each do |n|
if current_group.last == n
current_group << n
else
result << current_group
current_group = [n]
end
end
result << current_group
かなり長くなってしまいますね。
slice_when
を使えば、一行で同じことができます。
[1,1,2,2,2,3,4,4,4].slice_when {|a,b| a != b}
3. データ分析で重宝するメソッドたち
次は、データ分析で使えるメソッドを紹介します。
tally で要素をカウント
tally
は、各要素の出現回数をカウントしてくれるメソッドです。
Python を使っている人なら、collections.Counter
に似ているかもしれません。
['a', 'b', 'a', 'c', 'a'].tally
#=> {"a"=>3, "b"=>1, "c"=>1}
これは各要素の出現回数をカウントしてくれるメソッドです。
(Ruby 2.7 から使えるようになりました。)
もちろん、each
やtransform_values
を使っても同じことができます。
array = ['a', 'b', 'a', 'c', 'a']
counts = Hash.new(0)
array.each { |element| counts[element] += 1 }
# もしくは
array.group_by(&:itself).transform_values(&:size)
# もしくは
array.each_with_object(Hash.new(0)) { |element, counts|
counts[element] += 1
}
ただ、それなりに自分で書く必要があります。
一方、tally
を使えばこれが一行で書けます。
['a', 'b', 'a', 'c', 'a'].tally
chunk で連続する要素をまとめる
続いて、chunk
メソッドを紹介します。
chunk
は、連続する同じ要素をまとめてくれるメソッドです。
[1, 2, 2, 3, 3, 3].chunk(&:itself)
#=> [[1, [1]], [2, [2, 2]], [3, [3, 3, 3]]]
これは連続する同じ要素をまとめてくれるメソッドです。
たとえば、株価の上昇・下降トレンドを分析する場面を想像してみましょう。
普通なら、こうでしょうか。
ここでは、連続する同じ要素をまとめる処理を自分で書いています。
prices = [100, 110, 120, 115, 105, 95, 100, 110]
trends = []
current_trend = nil
current_values = []
prices.each_cons(2) do |a, b|
trend = a < b ? :up : :down
if current_trend == trend
current_values << [a, b]
else
trends << [current_trend, current_values] if current_trend
current_trend = trend
current_values = [[a, b]]
end
end
trends << [current_trend, current_values]
これを、chunk
を使えばもっとすっきり書けます。
prices = [100, 110, 120, 115, 105, 95, 100, 110]
prices.each_cons(2).chunk { |a, b| a < b ? :up : :down }
4. コードゴルフが捗るメソッドたち
コードをより短く書きたい人におすすめのメソッドを紹介します。
「コードゴルフ」とは、プログラムをできるだけ短く書くことを指します。
sum は文字列連結もできる
よく合計値を求めるために使うsum
メソッドですが、実は文字列連結にも使えます。
['Hello', ' ', 'World'].sum("")
#=> "Hello World"
文字列を連結するメソッドとして使えます。
普通なら、こうでしょうか。
['Hello', ' ', 'World'].join
# もしくは
['Hello', ' ', 'World'].reduce(:+)
sum
を使うと、初期値を指定して文字列連結ができます。
# 空文字列を初期値として指定
['Hello', ' ', 'World'].sum("")
flat_map は map して flatten するのと同じ
次に、flat_map
メソッドを紹介します。
flat_map
は、map
してからflatten
するのと同じことができます。
[[1,2], [3,4]].flat_map {|a| a.map {|n| n * 2}}
#=> [2, 4, 6, 8]
これは配列をフラットにしながら map する操作です。
もちろん、map
してからflatten
することもできます。
# mapしてからflatten
[[1,2], [3,4]].map {|a| a.map {|n| n * 2}}.flatten
# もしくは
result = []
[[1,2], [3,4]].each do |arr|
arr.each do |n|
result << n * 2
end
end
flat_map
を使えば一回のイテレーションで済むので、パフォーマンス的にも有利です。
[[1,2], [3,4]].flat_map {|a| a.map {|n| n * 2}}
5. 実務で使える便利メソッドたち
最後に、実務でよく使う(であろう)メソッドを紹介します。
grep_v は条件に合わないものを抽出
よく、特定の条件に合わない要素を抽出したいときがありますよね。
そんなときに使えるのが、grep_v
メソッドです。
numbers = [1, 2, 3, 4, 5]
numbers.grep_v(1..3) #=> [4, 5]
これは条件に「マッチしない」要素を抽出するメソッドです。
たとえば、1 から 3 までの数値を除外したいときに使えます。
numbers = [1, 2, 3, 4, 5]
numbers.reject { |n| (1..3).include?(n) }
# もしくは
numbers.select { |n| !(1..3).include?(n) }
grep_v
を使うと、より意図が明確になります。
# 無効なメールアドレスを見つける
emails = ["test@example.com", "invalid", "user@host"]
emails.grep_v(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
group_by でハッシュにグループ化
これもよく使うメソッドです。
[1, 2, 3, 4, 5, 6].group_by { |n| n % 3 }
#=> {1=>[1, 4], 2=>[2, 5], 0=>[3, 6]}
これは要素をグループ化してハッシュにしてくれるメソッドです。
たとえば、ユーザーを年齢層でグループ分けしたいときに使えます。
numbers = [1, 2, 3, 4, 5, 6]
result = Hash.new { |h, k| h[k] = [] }
numbers.each do |n|
result[n % 3] << n
end
group_by
を使えば、一行でグループ化できます。
# ユーザーを年齢層でグループ分け
users = [
OpenStruct.new(name: "Alice", age: 25),
OpenStruct.new(name: "Bob", age: 31),
OpenStruct.new(name: "Charlie", age: 28)
]
users.group_by { |user| user.age / 10 * 10 }
まとめ
いかがでしたか?
普段使っているeach
やmap
でも実現できる処理が、Enumerable の便利メソッドを使うともっと簡潔に書けることがわかりましたね。
まだまだ紹介しきれていない面白いメソッドがたくさんあります。
使えると思ったメソッドがあれば、ぜひ実際のコードで試してみてください!
他にもアドベントカレンダー記事を書いています!
他にも、2024 年のアドベントカレンダーに参加しています。
以下の記事でまとめているので、よければ他の記事も読んでいただけると嬉しいです!