Comparable and Enumerable in your own class

Comparable and <=>

A class that defines <=> and includes Comparable gets <, >, ==, between?, clamp and a working min and max:

class Box include Comparable attr_reader :n def initialize(n) @n = n end def <=>(other) n <=> other.n end end p Box.new(2) > Box.new(1)

ruby -e 'class Box; include Comparable; def initialize(n) = @n = n; attr_reader :n; def <=>(o) = @n <=> o.n; end; p Box.new(2) > Box.new(1)'

true

Only <=> is written by hand. The module defines the rest in terms of it, so there is one comparison rule and no chance of < disagreeing with sort. <=> returns -1, 0 or 1, and nil for two values that cannot be compared.

Enumerable and each

The same trick works for iteration. Define each and include Enumerable, and the class gains map, select, sum, sort, first, min_by and the rest of the toolbox:

class Bag include Enumerable def each yield 1 yield 2 end end p Bag.new.map { _1 * 2 } p Bag.new.sum p Bag.new.select(&:even?)

ruby -e 'class Bag; include Enumerable; def each; yield 1; yield 2; end; end; p Bag.new.map { _1 * 2 }; p Bag.new.sum; p Bag.new.select(&:even?)'

[2, 4]
3
[2]

each has to yield the items and nothing else: no return value matters, and a caller that passes a block gets the items one at a time. A collection that wraps an array usually delegates: @items.each { |item| yield item }, or @items.each(&block) with &block in the signature.

What you get for free

Two examples of the payoff, using the class above: a comparison that reads like a sentence, and a sorted collection with the smallest value:

ruby -e 'class Box; include Comparable; def initialize(n) = @n = n; attr_reader :n; def <=>(o) = @n <=> o.n; end; p [Box.new(3), Box.new(1)].min.n; p Box.new(2).clamp(Box.new(1), Box.new(3)).n'

1
2

ruby -e 'class Bag; include Enumerable; def each; yield 1; yield 2; end; end; p Bag.new.sort; p Bag.new.first; p Bag.new.min'

[1, 2]
1
1

None of those methods were written. That is the difference between a class that holds a collection and a collection: the second one speaks the language the rest of Ruby already uses.

What to watch for

Comparable gives == from <=>, but not eql? and not hash. A box used as a hash key still needs those two, which is the point of lesson 87 and, later, lesson 113.

An each that yields lazily from a file or a socket can be consumed only once. A caller that maps and then selects reads the stream twice and gets nothing the second time; read it into an array first, or design the class to be re-iterable.

Next: Modules as namespaces

Article author: Arthur Isaev

Banner image