アルゴリズム問題等でよく使うメソッド(Ruby)の使い方を、自分の忘備も兼ねて整理してみます。
警告
大変申し訳ありませんが、初心者が書いておりますので正確性には欠けますのでご注意ください。また、恐れ入りますが間違いがありましたらご指摘をお願いいたします。
前提
- Rubyは動的型付け言語なので、変数の型は実行時に決まります。 つまり obj.foo と書いたとき、obj が何のクラスのインスタンスかによって呼ばれるメソッドが変わります。
- 同じ名前のメソッドでも、レシーバーのクラスが違えば別メソッドです。例えばArray#eachとHash#eachは別メソッドです。
- Rubyでは「メソッド名」だけで一意に決まるのではなく、レシーバーのクラス階層を探索して、そのクラスやモジュールに定義されたメソッドが呼ばれる仕組みです。
- とはいえ、同じ名前で「似た意味」を持つメソッドを揃えることで、型を意識せずに統一的に扱うことができます。そのためわかりやすさの観点から、以下の記述でも同じ名前のメソッドをまとめて書いております。
- なお、各メソッドについて、ブロックを省略した場合の戻り値であるEnumerator(each 以外のメソッドにも Enumerable の機能を提供するためのラッパークラス)については記載を省略しています。
Enumerableモジュールについて
- Enumerableモジュールは、map, select, find, reduce など 繰り返し系の豊富なメソッド群を提供しています。
- Array, Hash, Range, Enumerator等のクラスで、 Enumerableモジュールはインクルードされていますのでそれらのメソッド群を使用可能です。
- ただし、効率化のため、そのクラスでEnumerableと同名・同等の機能を再定義(オーバーライド)しているケースも少なくありません。特に利用頻度の高いArrayなどのクラスでは、mapやselectといったメソッドをEnumerableの実装ではなく、より高速なC言語による独自実装でオーバーライドしているケースが多いです。
- なお、Enumerableモジュールのメソッドは全て each を用いて定義されているので、インクルードするクラスには each が定義されていなければなりません(当然ながらRuby組み込みのArray, Hash, Range等のクラスには定義されています)。
各メソッドの説明
each
Array#each
Hash#each
Range#each
[1, 2, 3].each { |n| puts n * 2 }
#=> 2, 4, 6 が順に出力
#戻り値 => [1, 2, 3]
- 戻り値はself(元のレシーバーそのもの)
- ブロック内で何か処理を書いてその結果を利用する副作用が目的。 ブロック自体が返す値を使うわけではないし、戻り値もレシーバーそのものなので使わない
times
5.times { |i| puts i }
#=> 0, 1, 2, 3, 4 が順に出力
#戻り値 => 5
- eachと同じく、 副作用が目的
map
Array#map
Enumrable#map(Hash,Range)
[1, 2, 3].map { |n| n * 2 }
#=> [2, 4, 6]
{a: 1, b: 2}.map { |k,v| [k, v*2] }
#=> [[:a, 2], [:b, 4]]
- ブロックの戻り値を集めて新しい配列を作る。レシーバーが何であっても戻り値は常に配列
select
Array#select
Hash#select
Enumerable#select (Range)
[1,2,3,4].select { |n| n.even? }
#=> [2, 4]
{a: 1, b: 2, c: 3}.select { |k, v| v > 1 }
#=> {:b=>2, :c=>3}
(1..10).select { |n| n.even? }
#=> [2, 4, 6, 8, 10]
- ブロックで真を返したものだけ集めて新しい配列orハッシュにする。レシーバーが配列かレンジなら配列を返し、レシーバーがハッシュならハッシュを返す。
group_by
Enumerable#group_by (Array,Hash,Range)
[1,2,3,4,5].group_by { |n| n.even? }
# ブロックの戻り値がキーになる:
# false => [1,3,5]
# true => [2,4]
# 結果
#=> {false=>[1,3,5], true=>[2,4]}
- ブロックの戻り値をキーにして、そのキーに対応する値を配列としてまとめたハッシュを返す
sort
[3,1,2].sort { |a, b| a <=> b }
#=> [1,2,3]
上記のコードの解説
-
<=> 演算子(宇宙船演算子)
- a <=> b で、a < b なら戻り値は-1、a = bなら戻り値は0、a > bなら戻り値は1
-
sortメソッド
- ブロックには2つの要素 (a, b) が渡される
- ブロックの戻り値が、負の値であれば、a は b より前に来る。戻り値が0であれば、順序は変わらない。戻り値が正の値であれば、a は b より後に来る。
- <=> 演算子がこの -1,0,1 を返すため、よくブロックで使われる。
-
以上を踏まえると、
- 3 <=> 1 の戻り値は 1 なので、 3 は 1 より後
- 1 <=> 2 の戻り値は -1 なので、 1 は 2 より前
- その結果、1,2,3と並べ替えられる
sort_by
Enumerable#sort_by (Array,Hash,Range)
[3,1,2].sort_by { |n| -n } # 降順にする場合
#=> [3,2,1]
上記のコードの解説
- ブロックの戻り値は、ソートキーとして使われる。 上記の例では、ブロックの戻り値(ソートキー)は順に-3, -1, -2
- 全てのソートキーを昇順に並べる (-3, -2, -1)
- 並べ替えたソートキーに対応する元の配列の要素を順番に並べる (3, 2, 1)
-
新しい配列として返す ([3, 2, 1])
※レシーバーがHashやRangeでも配列を返す
reduce
Enumerable#reduce (Array,Hash,Range)
# 合計
[1,2,3,4].reduce(0) { |sum, x| sum + x }
# => 10
# 値の合計
{a: 1, b: 2, c: 3}.reduce(0) { |sum, (k,v)| sum + v }
# => 6
- 引数を初期値として累積値(上記の例ならsum)に設定し、渡される各要素をブロックに適用しながら累積していき、最終的な値を返す
おわりに
まだまだ勉強中ですが、まずは基本的な部分を理解していきたいと思います。