Project

General

Profile

Actions

Feature #7604

open

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

Feature #7604: Make === comparison operator ability to delegate comparison to an argument

Added by prijutme4ty (Ilya Vorontsov) almost 13 years ago. Updated over 1 year ago.

Status:
Assigned
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

Updated by Anonymous almost 13 years ago Actions #1 [ruby-core:51090]

Your proposal reminds me of trying to extend #coerce behavior. What you call "mirroring", happens with #coerce. "Double mirrorring" is prevented by simply by #coerce being required to return a compatible pair. That being said, I did have times, when I wanted operator-specific #coerce (eg. different physical quantities do not add or compare, but do multiply). Essentially, you are proposing:

(1.) Let us have operator-specific #coerce (for #=== at least).
(2.) Let us have #=== actually using its specific coerce for some chosen argument types.

To me, achieving (1.) is imaginable as either #coerce taking an optional second argument, as in other.coerce( self, :=== ), or as having special #coerce_plus, #coerce_asterisk, #coerce_double_equal_sign, #coerce_triple_equal_sign etc.

Achieving (2.) is more difficult. As you pointed out, many classes have their own #===. But it is a general case that operator methods should be written with #coerce in mind.

Having thus reframed your proposal, let me also express my personal opinion about it: I would be in favor of cautiously implementing (1.), while (2.) means a bit work for everyone. I noticed that Marc Andre was also concerned about #coerce specification.

Updated by prijutme4ty (Ilya Vorontsov) almost 13 years ago Actions #2 [ruby-core:51094]

boris_stitnicky (Boris Stitnicky) wrote:

Your proposal reminds me of trying to extend #coerce behavior. What you call "mirroring", happens with #coerce. "Double mirrorring" is prevented by simply by #coerce being required to return a compatible pair. That being said, I did have times, when I wanted operator-specific #coerce (eg. different physical quantities do not add or compare, but do multiply). Essentially, you are proposing:

(1.) Let us have operator-specific #coerce (for #=== at least).
(2.) Let us have #=== actually using its specific coerce for some chosen argument types.

To me, achieving (1.) is imaginable as either #coerce taking an optional second argument, as in other.coerce( self, :=== ), or as having special #coerce_plus, #coerce_asterisk, #coerce_double_equal_sign, #coerce_triple_equal_sign etc.

Achieving (2.) is more difficult. As you pointed out, many classes have their own #===. But it is a general case that operator methods should be written with #coerce in mind.

Having thus reframed your proposal, let me also express my personal opinion about it: I would be in favor of cautiously implementing (1.), while (2.) means a bit work for everyone. I noticed that Marc Andre was also concerned about #coerce specification.

I like the idea of #coerce having additional argument(first time I thought whether current behavior of coerce can help me in solving this problem). Coercion implies that code of operators like + or === in built-in should be changed as in (2) case. I think that your solution can be actually much more flexible than mine. Also I can't realize any benefits of (2) over (1).

Updated by Anonymous almost 13 years ago Actions #3 [ruby-core:51117]

(2) and (1) are two steps of the same campaign, to make the behavior you described possible, but (1) might be easier and mildly useful on its own. Current #coerce would solve the problem provided that you make it return special objects with customized multiple operator methods, similar to your Predicate. Why not make a coerce-based gem demonstrating this? I would be interested in using it personally. You would have to find and patch those scattered #=== methods, while I am more interested in :+, :-, :*, :/, :**, and :<=>. We could have common special object for all of these.

Updated by prijutme4ty (Ilya Vorontsov) almost 13 years ago Actions #4 [ruby-core:51139]

boris_stitnicky (Boris Stitnicky) wrote:

(2) and (1) are two steps of the same campaign, to make the behavior you described possible, but (1) might be easier and mildly useful on its own. Current #coerce would solve the problem provided that you make it return special objects with customized multiple operator methods, similar to your Predicate. Why not make a coerce-based gem demonstrating this? I would be interested in using it personally. You would have to find and patch those scattered #=== methods, while I am more interested in :+, :-, :*, :/, :**, and :<=>. We could have common special object for all of these.

I will create a proof-of-concept gem, but not sure that I'll be able to create a native extension. So arithmetical operations can become much slower.

Updated by Anonymous almost 13 years ago Actions #5 [ruby-core:51148]

Let me know when you make the first commit.

Updated by ko1 (Koichi Sasada) over 12 years ago Actions #6

  • Category set to core
  • Target version set to 2.6

Updated by ko1 (Koichi Sasada) over 12 years ago Actions #7 [ruby-core:52662]

  • Assignee set to matz (Yukihiro Matsumoto)

Updated by prijutme4ty (Ilya Vorontsov) over 12 years ago Actions #8

boris_stitnicky (Boris Stitnicky) wrote:

Let me know when you make the first commit.

I released proof-of-concept gem. https://github.com/prijutme4ty/flex_coerce It makes no changes in behavior of base classes, you need to patch only your own class. But actually I didn't found any use-cases of this (e.g. physical quantities more naturally looks if there is a special quantity representing unity). I hope you'll hint me some good applications of this gem.
It's sad but #=== doesn't use coerce so this gem can't help me solve my task. So I'll soon create another gem that patches === method.

Updated by Anonymous over 12 years ago Actions #9 [ruby-core:53370]

@Ilya: I have noticed your post, I'll pay closer attention after next week.

Updated by Anonymous over 12 years ago Actions #10 [ruby-core:54184]

I'have started working on it (first I have to handle switch to 2.0 on my machine).

Updated by headius (Charles Nutter) over 12 years ago Actions #11 [ruby-core:54195]

As a feature that affects all Ruby implementations, this should probably move to CommonRuby: https://bugs.ruby-lang.org/projects/common-ruby

Updated by naruse (Yui NARUSE) almost 8 years ago Actions #12

  • Target version deleted (2.6)

Updated by hsbt (Hiroshi SHIBATA) over 1 year ago Actions #13

  • Status changed from Open to Assigned
Actions

Also available in: PDF Atom