Equality, Comparison, Hashing and Duck Typing

21.1 Equality Methods

Same object (identity) Strict equality used by Hash keys (no type coercion: 1.eql?(1.0) is false) Yes, with hash Hash code for Hash/Set membership Yes, with eql?

Case equality (used by case/when, grep) Ordering (with Comparable)

class Version
  include Comparable
  attr_reader :major, :minor, :patch

  def initialize(str) = @major, @minor, @patch = str.split(".").map(&:to_i)

  def <=>(other)
    return nil unless other.is_a?(Version)
    [major, minor, patch] <=> [other.major, other.minor, other.patch]
  end

  def eql?(other) = other.is_a?(Version) && (self <=> other).zero?
  def hash = [major, minor, patch].hash
  def to_s = [major, minor, patch].join(".")
end

versions = %w[1.10.0 1.2.3 1.2.10].map { Version.new(it) }
versions.sort.map(&:to_s)      # ["1.2.3", "1.2.10", "1.10.0"]
versions.max.to_s
{ Version.new("1.0.0") => "stable" }[Version.new("1.0.0")]   # "stable"

21.2 Conversion Protocols

Implicit (strict, "is a") Explicit (lenient, "can become") to_proc (used by &)

Implement coerce for numeric-like classes so 2 * my_obj works.

21.3 Duck Typing

"If it walks like a duck and quacks like a duck, it's a duck." Program to behavior, not to types.

def log_all(writer, messages)        # anything with #<< or #write works: File, StringIO, Array, Logger...
  messages.each { |m| writer << "#{m}\n" }
end
log_all($stdout, %w[a b])
log_all(StringIO.new, %w[c])
log_all([], %w[d])

def save(obj)
  raise TypeError, "needs #to_h" unless obj.respond_to?(:to_h)
  store(obj.to_h)
end