[FAQ] Defining <=>

At Gavin’s request here is a summary of my question and the answers I
received.

Q: I’ve defined a class whose objects may be ordered. What should
my <=> method do when the “other” argument is not in the same
class?

A. Raise a TypeError exception. The <=> method in most of Ruby’s
built-in classes will raise a TypeError exception if "other"
can’t be coerced into the same class.

If you want your class to include the other comparision operators
such as ==, <=, =>, etc., consider mixing in the Comparable module.
This module implements these other comparison operators by calling
your <=> method. For example,

class TestClass
include Comparable

def <=>(other)
unless other.kind_of? self.class
raise TypeError, "#{other.class} can’t be coerced into #{self.class}"
end

  # do whatever it takes to compare self <=> other

end

other methods

end

Hi –

At Gavin’s request here is a summary of my question and the answers I
received.

Q: I’ve defined a class whose objects may be ordered. What should
my <=> method do when the “other” argument is not in the same
class?

A. Raise a TypeError exception. The <=> method in most of Ruby’s
built-in classes will raise a TypeError exception if “other”
can’t be coerced into the same class.

If you want your class to include the other comparision operators
such as ==, <=, =>, etc., consider mixing in the Comparable module.
This module implements these other comparison operators by calling
your <=> method. For example,

class TestClass
include Comparable

def <=>(other)
unless other.kind_of? self.class
raise TypeError, “#{other.class} can’t be coerced into #{self.class}”

I’m not sure this error message makes sense. All you’ve discovered is
that the other object is not of the same class as the current object;
there’s been no further test or coercion attempted. In fact, some
objects are happy to masquerade as other objects:

class S
def to_str
“ABC”
end
end

s = S.new
p s.kind_of?(String) # false
p Regexp.new(s) # /ABC/
p “Some string” <=> s # 1

or simply to respond to the necessary messages without being of the
same class. This may or may not be what you want in a given case, but
I think the kind_of? test probably isn’t an all-purpose way to
conceptualize the <=> implementation.

David

···

On Sat, 23 Nov 2002, Tim Hunter wrote:


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav

“Tim Hunter” cyclists@nc.rr.com writes:

Q: I’ve defined a class whose objects may be ordered. What should
my <=> method do when the “other” argument is not in the same
class?

A. Raise a TypeError exception.

I have to say I disagree with this fairly strongly.

The <=> method in most of Ruby’s
built-in classes will raise a TypeError exception if “other”
can’t be coerced into the same class.

There’s a boat load of difference between coercion and “being the same
class”. For example, following your code example would mean that

1 <=> 1.2

would raise a type error.

I honestly believe that the correct thing to do is not to check the
class (or the methods implemented) of your argument. Instead, assume
the person using your class has half a brain. They may deliberately
pass a String in to a method designed to work with Files, knowing that
the << operator works fine with both. They may well pass a Float into
class Fixnum’s <=> method, knowing that the right thing will be done.

You might choose to implement coercion in your own class if you feel
it worthwhile. You can do something as simple as calling a ‘to_xxx’
method, or you can use something similar to the Numeric coerce
scheme. Depending on the circumstances, this might be a friendly move.

Overzealously checking the class of parameters defeats much of the
point of Ruby.

Regards

Dave

“Tim Hunter” cyclists@nc.rr.com writes:

Q: I’ve defined a class whose objects may be ordered. What should
my <=> method do when the “other” argument is not in the same
class?

A. Raise a TypeError exception.

I have to say I disagree with this fairly strongly.

The <=> method in most of Ruby’s
built-in classes will raise a TypeError exception if “other”
can’t be coerced into the same class.

There’s a boat load of difference between coercion and “being the same
class”. For example, following your code example would mean that

1 <=> 1.2

would raise a type error.

[…]

These are good points, Dave (and David Black as well), and I do not disagree,
but I will point out that the counter-examples given are strongly in the domain
of built-in classes (i.e. String <=> Regexp, Integer <=> Float), and this is no
surprise: the classes built in to the langauge are obviously designed to work
together to a degree.

The FAQ is about user-defined classes, and I’ll make the following points in
support of type-checking in the comparison operator:

  • things that can be compared are generally data objects more than behaviour
    objects
  • it is behaviour objects that are capable of masquerading, so the points
    raised by Davids would apply mostly to those
  • user-defined data objects are generally quite application-, or domain-
    specific
  • application- or domain-specific objects are generally not designed to
    be compared to built-ins or other user-defined objects [1]; obviously,
    exceptions exist and this should be noted
  • coercion between user-defined types is a rare thing, and the programmer
    would obviously make use of it in comparison if it exists
  • if the relationship between user-defined types is known (and it should be)
    why shouldn’t it be encoded/enforced?
  • the price of not checking the class of the object to be compared is
    possibly calling methods that don’t exist, or that do the “wrong” thing
    (e.g. consumed by method_missing)
  • the benefit of checking the class of the object to be compared is clearer
    error messages and prevention of doing The Wrong Thing
  • the cost of checking the class of the object to be compared, after
    considering all of the above, is … what?

Overzealously checking the class of parameters defeats much of the
point of Ruby.

Dave

To which I say “everything in moderation”. I perform class-checking on my
data-objects when I have considered all the above and conclude that there is no
good reason not to. Comparison seems like an important part of program logic,
and I like to catch errors early. Especially after modifying something -
comparison is something that happends “downstream”, nowhere near the actual
modification. Also, I like meaningful error messages where possible.

I also like Ruby’s flexibility, so I don’t overdo it, and take advantage of the
flexibility (at the risk of weird errors) when I can.

Cheers,
Gavin

PS. I don’t know what this means for the FAQ. A bit of discussion, I suppose.

[1] A user-defined data object would generally have several (N>2) fields.
Comparison with another object would generally involve most of these N fields.
Thus comparisons with built-in types (generally 1 field) are not meaningful
(again, exceptions exist, but they are exceptions). Also, a different
user-defined object would only compare meaningfully if it was known to be
related in some way.

···

From: “Dave Thomas” Dave@PragmaticProgrammer.com

“Gavin Sinclair” gsinclair@soyabean.com.au writes:

  • the cost of checking the class of the object to be compared,
    after considering all of the above, is … what?

My main objection is that you remove flexibility from the future users
of your class. Insisting that classes you write will only work with
each other makes them closed.

Promoting type checking based on #kind_of? in the FAQ runs counter to
the style of Ruby (in my opinion).

Cheers

Dave

The fact that there are several divergent views seems to point in the
diretion of a non-authoritative entry: “This is is a disputed issue.
[Explanation of main approaches and their pros and cons.]”

– Nikodemus

···

On Mon, 25 Nov 2002, Gavin Sinclair wrote:

PS. I don’t know what this means for the FAQ. A bit of discussion, I
suppose.

Hi –

PS. I don’t know what this means for the FAQ. A bit of discussion, I
suppose.

The fact that there are several divergent views seems to point in the
diretion of a non-authoritative entry: “This is is a disputed issue.
[Explanation of main approaches and their pros and cons.]”

To the extent that the discussion has revolved around the practice of
explicity testing for type in Ruby, I think it’s misleading to file it
under a FAQ about implementing <=>, since it’s a much broader matter.
I’m actually not in favor of having “kind_of?” branching recommended
in the FAQ at all, both for the reasons Dave has mentioned and because
I think it tends to stop one from thinking further, and in ways that
take better advantage of Ruby, about one’s design. But in any case it
isn’t a <=>-specific practice.

David

···

On Mon, 25 Nov 2002, Nikodemus Siivola wrote:

On Mon, 25 Nov 2002, Gavin Sinclair wrote:


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav

I'm actually not in favor of having "kind_of?" branching recommended
in the FAQ at all, both for the reasons Dave has mentioned and because

Well, you can remove it from the FAQ but you'll never stop ruby to do this

pigeon% ruby -e 'File.open()'
-e:1:in `open': wrong argument type Array (expected String) (TypeError)
        from -e:1
pigeon%

Guy Decoux

Hi –

I’m actually not in favor of having “kind_of?” branching recommended
in the FAQ at all, both for the reasons Dave has mentioned and because

Well, you can remove it from the FAQ but you’ll never stop ruby to do this

pigeon% ruby -e ‘File.open()’
-e:1:in `open’: wrong argument type Array (expected String) (TypeError)
from -e:1

True, and it serves a purpose, but I wouldn’t want to use that as a
model for how to design and handle classes in Ruby. For one thing,
it doesn’t even recognize Ruby’s own String masquerading:

class Thing
def to_str
“filename”
end
end

File.open(Thing.new, “w”)

=> op.rb:7:in `open’: wrong argument type Thing (expected String)
(TypeError)
from op.rb:7

so I would tend to regard it as a quite special case.

David

···

On Mon, 25 Nov 2002, ts wrote:


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav

Hi,

···

In message “Re: [FAQ] Defining <=>” on 02/11/25, ts decoux@moulon.inra.fr writes:

Well, you can remove it from the FAQ but you’ll never stop ruby to do this

pigeon% ruby -e ‘File.open()’
-e:1:in `open’: wrong argument type Array (expected String) (TypeError)
from -e:1

But it based on whether object has “to_str” or not. I know Ruby still
does type testing. But it is evloving.

						matz.

Hi –

···

On Tue, 26 Nov 2002, Yukihiro Matsumoto wrote:

Hi,

In message “Re: [FAQ] Defining <=>” > on 02/11/25, ts decoux@moulon.inra.fr writes:

Well, you can remove it from the FAQ but you’ll never stop ruby to do this

pigeon% ruby -e ‘File.open()’
-e:1:in `open’: wrong argument type Array (expected String) (TypeError)
from -e:1

But it based on whether object has “to_str” or not. I know Ruby still
does type testing. But it is evloving.

This made me take a second look at my to_str example… In 1.6.7
File#open doesn’t seem to look for to_str, but it does in 1.7. So
I semi-retract my previous observation :slight_smile:

David


David Alan Black
home: dblack@candle.superlink.net
work: blackdav@shu.edu
Web: http://pirate.shu.edu/~blackdav

matz@ruby-lang.org (Yukihiro Matsumoto) writes:

Hi,

Well, you can remove it from the FAQ but you’ll never stop ruby to do this

pigeon% ruby -e ‘File.open()’
-e:1:in `open’: wrong argument type Array (expected String) (TypeError)
from -e:1

But it based on whether object has “to_str” or not.

Actually, File.open looks for a String, it just calls
rb_check_safe_str. Arguably to should use to_str, though.

Cheers

Dave

···

In message “Re: [FAQ] Defining <=>” > on 02/11/25, ts decoux@moulon.inra.fr writes:

Hi,

···

In message “Re: [FAQ] Defining <=>” on 02/11/26, Dave Thomas Dave@PragmaticProgrammer.com writes:

But it based on whether object has “to_str” or not.

Actually, File.open looks for a String, it just calls
rb_check_safe_str. Arguably to should use to_str, though.

It does. Everything I say is based on 1.7 in general. Sorry for
inconvenience.

						matz.

But it based on whether object has “to_str” or not. I know Ruby still
does type testing. But it is evloving.

This made me take a second look at my to_str example… In 1.6.7
File#open doesn’t seem to look for to_str, but it does in 1.7. So
I semi-retract my previous observation :slight_smile:

:slight_smile:

coming from a bit different background (i.e. mainly java) i find this
discussion interesting. if i would follow this policy in java, i would
change method arguments of type java.lang.String into java.lang.Object and
internally invoke String.valueOf( o ) or toString() if i want to recognize
'null’s. this seems odd to me since i would loose information about the
interface. and since toString() is defined in class Object, everybody can
do this conversion easily before the method call. so - at least for java -
i would opt against this policy.

i’d like to know, what you folks say about this: is it a general matter or
is this distinction connected to the differences between Java and Ruby?

thanks!

robert