プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

Enumerableモジュールのメソッドツアー - maxメソッド

Enumerableモジュールのメソッドを1記事1メソッドで雑に紹介していくコーナー第2弾です。

前回はall?メソッドを紹介しました。

haruguchi-yuma.hatenablog.com

今回はmaxメソッドについて紹介していきます。

maxメソッド

まずはるりま*1の確認から。

module Enumerable (Ruby 3.1 リファレンスマニュアル)

ブロックの有無で説明が分かれていました。

詳しい説明はるりまに譲るとして、雑に紹介するなら、最大の要素を引数の数だけ返すメソッドということになります。

最大の要素がなければ、nilを返します。

早速irbなどで試してみましょう。

irbで挙動の確認。

Ruby3.1.2で確認しています。

Enumerable#maxの説明ではありますが、ArrayやRangeクラスなどオーバーライドしているmaxメソッドも併せて紹介します。

ブロックなしの場合

ArrayやRangeオブジェクトに対して引数ありと引数なしでmaxメソッドを呼び出しています。

[1, 2, 3, 4, 5].max
=> 5

[1, 2, 3, 4, 5].max(2)
=> [5, 4]

(1..10).max
=> 10

(1..10).max(3)
=> [10, 9, 8]

('a'..'z').max
=> "z"

('a'..'a').max
=> 'a'

[].max
=> nil

引数なしの場合は返り値は最大の要素そのものが返ってきており、最大が見つからない(空配列など)場合はnilを返していることがわかります。 引数ありの場合は返り値は配列です。引数で指定した要素数だけ最大値を選び、降順に並べて返します。

# 引数なし
[].max
=> nil

(2..1).max
=> nil

# 引数あり
[].max(2)
=> []

(2..1).max(2)
=> []

上のコードを試して個人的にはびっくりしたポイントです。引数有りの場合は最大が見つからなくても配列を返すというのがわかりやすいです。

難しい話ですが、maxメソッドで最大の要素とは何かというと<=>で比較できるものみたいですね。 module Comparable (Ruby 3.1 リファレンスマニュアル)

100 <=> 1 # 比較できる
=> 1

1.2 <=> 1 # 比較できる
=> 1

'a' <=> 'aa' # 比較できる
=> -1

true <=> false # 比較できない
=> nil

1 <=> 'a' # 比較できない
=> nil

上の例でいくとbooleanは比較できないのでmaxメソッドで最大の要素は取得できません。(true <=> trueは0なので等価性は比較できる)

また、Integerとstringなど型が違う場合(Integerとfloatは除く)<=>で比較できないのでmaxメソッドは例外を発生させます。

 [true, false, true, false].max
=> comparison of TrueClass with false failed (ArgumentError)

[1, 'a', 3,5, false].max
=> comparison of String with 1 failed (ArgumentError)

Hashはmaxメソッドをオーバーライドしていません。

{a: 3, b: 2, c: 1}.max
=> [:c, 1]

{a: 3, b: 2, c: 1}.invert.max # invertはkey valueをひっくり返すメソッド
=> [3, :a]

{a: 3, b: 2, c: 1}.max(2)
=> [[:c, 1], [:b, 2]]

おそらくですが、keyで大小比較をして、[key, value]の配列(つまり要素)で返しているみたいです。

ブロック有りの場合

るりまの説明です。

ブロックの評価結果で各要素の大小判定を行い、最大の要素、もしくは最大の n 要素が入った降順の配列を返します。引数を指定しない形式では要素が存在しなければ nil を返します。引数を指定する形式では、空の配列を返します。ブロックの値は、a > b のとき正、 a == b のとき 0、a < b のとき負の整数を、期待しています。該当する要素が複数存在する場合、どの要素を返すかは不定です。

なんだか難しいですが、ブロックパラメータには要素が2つずつ入ってきます。この2つの要素をブロック内で比較して左辺が大きい場合は正の数を返し、左辺が小さい場合は負の数を返し、左辺も右辺も等しい場合は0を返すようにすればOKです。そうすることで「どの観点で最大値を取るか」を柔軟に指定することができます。

文字列なら通常は辞書順ですが、ここでは文字の長さで最大値を取りたいとします。つまり、文字列が長い方が最大であると定義します。

['monkey' 'Gorilla gorilla', 'cat'].max
=> "monkey" # この場合は辞書順

['monkey', 'Gorilla gorilla', 'cat'].max do |a, b|
  if  a.length > b.length
    1000 # 左辺が大きい場合 正の数を返す
  elsif a.length < b.length
    -1000 # 左辺が小さい場合 負の数を返す
  else
    0 # 等しい場合 0を返す
  end
end
=> "Gorilla gorilla"

今回は文字数で比較したいのでlengthメソッドを利用しています。

ブロックの値は、a > b のとき正、 a == b のとき 0、a < b のとき負の整数を、期待しています。

ブロックの評価で数値を返す部分は適当な数を返しましたが、本来<=>を使うともっと簡単に記述することができます。

['monkey', 'Gorilla gorilla', 'cat'].max { |a, b| a.length <=> b.length }
=> "Gorilla gorilla"

ブロックを渡すことで最大とは何か?自分で定義できるのがいいところですね。 下の例は文字列の最後の文字を辞書順に比較する例です。 (そんなことしたい場合はなさそう😅)

['monkey', 'Gorilla gorilla', 'cat'].max { |a, b| a[-1] <=> b[-1] }
=> 'monkey'

monkeyの最後の文字が'y'なのでうまくいってるようです。

まとめ

雑にまとめます。

  • maxメソッドは最大の要素を返す
  • 引数を指定するとその指定した引数の要素数だけ配列にして返す(降順)
  • ブロックを渡すことができる -ブロックでは2つの要素を比較し、左辺が大きければ正の数、左辺が小さければ負の数、左辺と右辺が等しければ0を返す
  • そういう決まりを守ることで最大とは何かを柔軟に定義できる

個人的にmax_byとよく混同してしまうので今回整理できてよかったです。

*1:Rubyリファレンスマニュアルの略