In Ruby, why does playlist.max or playlist.sort raise ArgumentError on an Enumerable Playlist of Track objects, and what fixes it?
answer
- the elements are compared, not the Playlist
- Object#<=> returns nil unless ==
- comparison of Track with Track failed
- define <=> on Track
- or use sort_by and max_by
basics
~20 ssort, min, max and minmax compare the elements with <=>, and a plain Track inherits Object#<=>, which returns nil for different objects, so the call raises ArgumentError. Define <=> on Track, or pass a key with sort_by or max_by.
solid answer
~40 sEnumerable's `sort`, `min`, `max` and `minmax` compare the **elements** that `each` yields, using `<=>`. A `Track` class (or a `Data.define` value) without its own `<=>` inherits `Object#<=>`, which returns `0` only for the same object or an `==` equal one and `nil` otherwise, so the comparison fails with `ArgumentError: comparison of Track with Track failed`. Defining `<=>` on the **Playlist** does not help, because that compares playlists to each other. Two fixes: give `Track` a `<=>` that defines its natural order (usually with `Comparable`), or, when there is no single natural order, keep `Track` as is and pass the ordering at the call site with `sort_by`, `max_by` or a comparator block to `sort`/`max`.
code
ruby · 24 linesTrack = Data.define(:title, :seconds)
class Playlist
include Enumerable
def initialize(tracks) = (@tracks = tracks)
def each(&block)
return enum_for(:each) unless block
@tracks.each(&block)
self
end
end
list = Playlist.new([Track.new("Intro", 95), Track.new("Theme", 240)])
begin
list.max
rescue ArgumentError => e
puts e.message # comparison of Track with Track failed
end
p list.max_by { |t| t.seconds }.title # => "Theme"
p list.min { |a, b| a.title <=> b.title }.title # => "Intro"go deeper
Recall that sort, min and max compare the elements with <=>, and that max_by or sort_by with a block avoids needing it.
Explain Object#<=> returning nil for unrelated objects, why that raises ArgumentError, and why <=> on the collection class does not help.
Choose between a natural order on the element and call-site keys based on how many orderings the domain has, and guard mixed-type collections.
Set a convention for when value objects get a natural order so sorting behaviour stays predictable across teams and services.
## Where the comparison happens Including `Enumerable` gives a Playlist `sort`, `min`, `max` and `minmax`. None of them compare Playlists; they compare the **elements** yielded by `each`, pairwise, with the `<=>` (spaceship) operator. ```ruby Track = Data.define(:title, :seconds) playlist = Playlist.new([Track.new("Intro", 95), Track.new("Theme", 240)]) playlist.max # ArgumentError: comparison of Track with Track failed ``` ## Why the error appears Every object has a `<=>`, inherited from `Object`. Its rdoc in `object.c` says it returns: - `0` if the two are the same object, or if `self == other`; - `nil` otherwise. `nil` means "these cannot be ordered". When `max` or `sort` gets `nil` back, CRuby's comparison helper raises `ArgumentError` with `comparison of X with Y failed`. So the error is not about a missing method; `<=>` exists but has no ordering to offer. ## Fix 1: give the element a natural order If tracks have one obvious order, define `<=>` on `Track`: ```ruby Track = Data.define(:title, :seconds) do include Comparable def <=>(other) = [seconds, title] <=> [other.seconds, other.title] end playlist.max.title # => "Theme" playlist.sort # Array of tracks, shortest first ``` - `<=>` must return a negative number, zero, a positive number, or `nil` for incomparable values. - Including `Comparable` adds `<`, `>`, `between?` and `clamp` on top of it; how `Comparable` and `<=>` interact with equality belongs to the material on object equality. ## Fix 2: order at the call site Often there is no single natural order for a track: by length, by title, by play count. Then leave `Track` alone and pass the rule each time: | Need | Call | |---|---| | longest track | `playlist.max_by { \|t\| t.seconds }` | | tracks by title | `playlist.sort_by { \|t\| t.title }` | | custom comparator | `playlist.max { \|a, b\| a.seconds <=> b.seconds }` | | shortest and longest | `playlist.minmax_by { \|t\| t.seconds }` | These never call `Track#<=>`, so they work on any element type. ## A common wrong fix Developers sometimes add `<=>` to the **Playlist** class. That makes `playlist_a <=> playlist_b` meaningful and lets an Array of playlists be sorted, but `playlist.max` still compares tracks and still fails. ## Other element requirements Ordering is one of several places where Enumerable methods lean on the elements: 1. `sort`, `min`, `max`, `minmax` need `<=>` that returns numbers. 2. `include?` uses `==`. 3. `uniq`, `tally` and `group_by` keys rely on hashing. 4. `sum` without a block uses `+` starting from `0`, so `playlist.sum` raises `TypeError` for Tracks; `playlist.sum { |t| t.seconds }` works. `Data.define` already gives `==`, `eql?` and `hash` based on the members, so the ordering methods and `sum` are the usual surprises. ## Mixed element types A collection holding different classes, such as tracks and podcast episodes, fails the same way unless their `<=>` implementations accept each other. In that case the call-site form with a shared key (`max_by { |item| item.seconds }`) is the robust choice. ## Summary Enumerable orders **elements**, via `<=>`. Give the element class a `<=>` when it has a natural order, otherwise use `sort_by`, `max_by` or a comparator block, and never expect `<=>` on the collection class to help.
- Why does Object#<=> return 0 for a Track compared with itself but nil for a different Track?`Object#<=>` only knows identity and `==`: it returns 0 when the objects are the same or `==` equal, and nil otherwise, meaning no order is defined. For `Data` values with equal members, `==` is true, so even two distinct but equal tracks compare as 0.
- When would you choose max_by over defining Track#<=>?When tracks have no single natural order, for example longest in one screen and alphabetical in another. A call-site key keeps each rule explicit and avoids a `<=>` that silently favours one ordering everywhere, including in `sort` calls elsewhere.
saying these in an interview costs you the question
- Defining <=> on the Playlist class makes playlist.max work
- Track raises NoMethodError because objects have no <=> by default
- max_by also needs Track#<=> to be defined
- Including Comparable in Playlist fixes sort on its tracks
- Data.define classes are ordered by their members automatically