Project

General

Profile

Feature #7604

Make === comparison operator ability to delegate comparison to an argument

Added by prijutme4ty (Ilya Vorontsov) almost 8 years ago. Updated almost 3 years ago.

Status:
Open
Priority:
Normal
Target version:
-
[ruby-core:51076]

Description

=begin
I propose to expand default behaviour of === operator in the following way:
Objects have additional instance method Object#reverse_comparison?(other) which is false by default in all basic classes.
Each class that overrides Object#===(other) should check whether reverse_comparison? is true or false
If it is false, behavior is not changed at all.
If it is true, comparison is delegated to === method of an argument with self as an argument.

This technique can help in constructing RSpec-style matchers for case statement. Example:

# usual method call
arr = %w[cat dog rat bat]
puts arr.end_with?(%w[dog bat]) # ==> false
puts arr.end_with?(%w[rat bat]) # ==> true
puts arr.end_with?(%w[bat]) # ==> true

# predicate-style case
case %w[cat dog rat bat].end_with?
when %w[dog bat]
puts '..., dog, bat'
when %w[rat bat]
puts '..., rat, bat'
when %w[bat]
puts '..., bat'
else
puts 'smth else'
end
# ==> ..., rat, bat

Code needed to run this is not very complex:
class Object
def reverse_comparison?(other)
false
end
alias_method :'old===', :'==='
def ===(other)
(other.reverse_comparison?(self) ? (other.send 'old===',self) : (self.send 'old===',other))
end
end

class Predicate
def initialize(&block)
@block = block
end
def reverse_comparison?(other)
true
end
def ===(*args)
@block.call(*args)
end
end

class Array
alias_method :'old===', :'==='
def ===(other)
other.reverse_comparison?(self) ? (other.send('===',self)) : (self.send('old===',other))
end

def end_with?(expected_elements = nil)
  return last(expected_elements.size) == expected_elements  if expected_elements
  Predicate.new{|suffix| last(suffix.size) == suffix }
end

end

This technique looks powerful and beautiful for me. One detail is that obj#reverse_comparison? can distinguish different types of arguments and returns true only for certain types of given object. Also this can be used to prevent double-mirroring (as shown below)

The problem is that many base classes already defined custom === operator, so each of those classes (Fixnum, Float, String, Regexp, Range etc) should be redefined in such a way to make a solution full-fledged.
Another problem is case that both objects defined reverse_comparison? to return true. In my solution Predicate#=== just ignores result of revese_comparison? which is not consistent.
Another possible way is to raise errors on double mirroring:
def reverse_comparison?(other)
raise 'double mirroring' if @mirroring_started
@
mirroring_started = true
return true unless other.reverse_comparison?(self)
false
ensure
remove_instance_variable :@__mirroring_started
end

My proposal is to add reverse_comparison? method and change base classes operator === to use its result as shown above. May be it's worth also to make a class analogous to Predicate in stdlib.
=end

Also available in: Atom PDF