バージョン情報
$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
例
Enumerable#sort_by のブロックの評価結果が配列で、かつ配列の要素に nil を含む場合に "comparison of Array with Array failed" というエラーが発生する。
Pokemon = Data.define(:name, :type1_id, :type2_id)
# 例 1: OK
pokemons = [
Pokemon.new(name: 'ジュナイパー', type1_id: 5, type2_id: 14),
Pokemon.new(name: 'ゲッコウガ', type1_id: 3, type2_id: 16),
Pokemon.new(name: 'フシギバナ', type1_id: 5, type2_id: 8)
]
pokemons.sort_by { [_1.type1_id, _1.type2_id] }.map(&:name)
#=> ["ゲッコウガ", "フシギバナ", "ジュナイパー"]
# 例 2: OK
pokemons = [
Pokemon.new(name: 'ジャローダ', type1_id: 5, type2_id: nil),
Pokemon.new(name: 'ゲッコウガ', type1_id: 3, type2_id: 16),
Pokemon.new(name: 'バクフーン', type1_id: 2, type2_id: nil)
]
pokemons.sort_by { [_1.type1_id, _1.type2_id] }.map(&:name)
#=> ["バクフーン", "ゲッコウガ", "ジャローダ"]
# 例 3: NG
pokemons = [
Pokemon.new(name: 'ジャローダ', type1_id: 5, type2_id: nil),
Pokemon.new(name: 'ゲッコウガ', type1_id: 3, type2_id: 16),
Pokemon.new(name: 'フシギバナ', type1_id: 5, type2_id: 8)
]
pokemons.sort_by { [_1.type1_id, _1.type2_id }.map(&:name)
# `sort_by': comparison of Array with Array failed (ArgumentError)
理由
ブロックの評価結果の配列に nil が含まれる場合、かつ nil と nil でないオブジェクトの比較が行われる際にエラーが発生する。例えば上記のコードでは、例 1 と例 2 の sort_by では nil と nil でないオブジェクトの比較は行われない。しかし例 3 の場合は行われる。
Enumerable#sort_by では値の比較時に <=> 演算子 (比較演算子) を使用する。この <=> は比較可能な場合は整数値を返し、不可能な場合は nil を返す。例えば nil と整数値は比較ができない。
module Comparable (Ruby 3.2 リファレンスマニュアル) より
self <=> other は
- self が other より大きいなら正の整数
- self と other が等しいなら 0
- self が other より小さいなら負の整数
- self と other が比較できない場合は nil
をそれぞれ返すことが期待されています。
[5, nil] <=> [5, 8]
#=> nil
5 <=> 5
#=> 0
nil <=> 8
#=> nil
ちなみに true と false も比較できない。
false <=> true
#=> nil
[[2, true], [1, false], [2, false]].sort_by(&:itself)
# `sort_by': comparison of Array with Array failed (ArgumentError)
どうしても比較したいのであれば、比較可能なオブジェクト同士 (整数と整数など) で比較するように工夫するとよい。
pokemons = [
Pokemon.new(name: 'ジャローダ', type1_id: 5, type2_id: nil),
Pokemon.new(name: 'ゲッコウガ', type1_id: 3, type2_id: 16),
Pokemon.new(name: 'フシギバナ', type1_id: 5, type2_id: 8)
]
pokemons.sort_by { [_1.type1_id, _1.type2_id }.map(&:name)
# `sort_by': comparison of Array with Array failed (ArgumentError)
pokemons.sort_by { [_1.type1_id, (_1.type2_id || 0)] }.map(&:name)
#=> ["ゲッコウガ", "ジャローダ", "フシギバナ"]
[[2, true], [1, false], [2, false]].sort_by(&:itself)
# `sort_by': comparison of Array with Array failed (ArgumentError)
[[2, true], [1, false], [2, false]].sort_by { [_1, _2 ? 1 : 0] }
#=> [[1, false], [2, false], [2, true]]