Coercion in Ruby

A fellow rubyist, Zach Church, wrote a great article on coercion in
Ruby. It's not something I've seen covered often so I thought I'd
share with the community at large:

Zach Dennis

Thanks for sharing, very well done.

···

On Wed, Jan 26, 2011 at 8:58 AM, zdennis <zach.dennis@gmail.com> wrote:

A fellow rubyist, Zach Church, wrote a great article on coercion in
Ruby. It's not something I've seen covered often so I thought I'd
share with the community at large:

http://mutuallyhuman.com/blog/2011/01/25/class-coercion-in-ruby

Zach Dennis

In case you weren't aware: I covered this thoroughly a while ago when
I described what it needs to create a numeric class which nicely plays
along with other numeric classes.

http://blog.rubybestpractices.com/posts/rklemme/019-Complete_Numeric_Class.html

Cheers

robert

···

On Wed, Jan 26, 2011 at 3:58 PM, zdennis <zach.dennis@gmail.com> wrote:

A fellow rubyist, Zach Church, wrote a great article on coercion in
Ruby. It's not something I've seen covered often so I thought I'd
share with the community at large:

http://mutuallyhuman.com/blog/2011/01/25/class-coercion-in-ruby

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Just a small nitpick:

  if other.is_a? TimeInterval
    TimeInterval.new(value + other.value)
  elsif other.is_a? Numeric
    TimeInterval.new(value + other)
  else
    raise TypeError, "#{other.class} can't be coerced into TimeInterval"
  end

would probably be better written as:

  case other
  when TimeInterval
    TimeInterval.new(value + other.value)
  when Numeric
    TimeInterval.new(value + other)
  else raise TypeError, "#{other.class} can't be coerced into TimeInterval"
  end

···

On Wed, Jan 26, 2011 at 7:58 AM, zdennis <zach.dennis@gmail.com> wrote:

A fellow rubyist, Zach Church, wrote a great article on coercion in
Ruby. It's not something I've seen covered often so I thought I'd
share with the community at large:

http://mutuallyhuman.com/blog/2011/01/25/class-coercion-in-ruby

Zach Dennis

--
Tony Arcieri
Medioh! Kudelski

Thanks Robert, I hadn't seen your post before. Will add to my reader docket!

Zach

···

On Wed, Jan 26, 2011 at 10:29 AM, Robert Klemme <shortcutter@googlemail.com> wrote:

On Wed, Jan 26, 2011 at 3:58 PM, zdennis <zach.dennis@gmail.com> wrote:

A fellow rubyist, Zach Church, wrote a great article on coercion in
Ruby. It's not something I've seen covered often so I thought I'd
share with the community at large:

http://mutuallyhuman.com/blog/2011/01/25/class-coercion-in-ruby

In case you weren't aware: I covered this thoroughly a while ago when
I described what it needs to create a numeric class which nicely plays
along with other numeric classes.

http://blog.rubybestpractices.com/posts/rklemme/019-Complete_Numeric_Class.html

Cheers

robert

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

--
Zach Dennis
http://www.continuousthinking.com (personal)
http://www.mutuallyhuman.com (hire me)
http://ideafoundry.info/behavior-driven-development (first rate BDD training)
@zachdennis (twitter)

Or at least:

if other.kind_of? TimeInterval
  ...
elsif other.kind_of? Numeric
  ...

Remember duck typing? I don't care if it is_a Numeric, not really. I can't
really think of many cases where is_a? makes more sense than kind_of?.

···

On Wednesday, January 26, 2011 02:52:12 pm Tony Arcieri wrote:

Just a small nitpick:

  if other.is_a? TimeInterval
    TimeInterval.new(value + other.value)
  elsif other.is_a? Numeric
    TimeInterval.new(value + other)
  else
    raise TypeError, "#{other.class} can't be coerced into TimeInterval"
  end

would probably be better written as:

  case other
  when TimeInterval
    TimeInterval.new(value + other.value)
  when Numeric
    TimeInterval.new(value + other)
  else raise TypeError, "#{other.class} can't be coerced into TimeInterval"
  end

Good point, but that said, it can be done even better:

First, in TimeInterval, alias_method :to_int, :value

Then when implementing +:

if other.respond_to? :to_int
  TimeInterval.new(value + other.to_int)
elsif other.respond_to? :coerce
  a, b = other.coerce(self)
  a + b
else raise TypeError, "#{other.class} can't be coerced into TimeInterval"
end

···

On Wed, Jan 26, 2011 at 9:20 PM, David Masover <ninja@slaphack.com> wrote:

Or at least:

if other.kind_of? TimeInterval
...
elsif other.kind_of? Numeric
...

Remember duck typing?

--
Tony Arcieri
Medioh! Kudelski

Am I missing something here? I don't see how this is in keeping with the
idea of duck typing: is_a? is an alias for kind_of? and vice-versa.

http://www.ruby-doc.org/core/classes/Object.html#M001034

Perhaps you were thinking of instance_of? here?

···

On Thu, Jan 27, 2011 at 4:20 AM, David Masover <ninja@slaphack.com> wrote:

On Wednesday, January 26, 2011 02:52:12 pm Tony Arcieri wrote:
> Just a small nitpick:
>
> if other.is_a? TimeInterval
> TimeInterval.new(value + other.value)
> elsif other.is_a? Numeric
> TimeInterval.new(value + other)
> else
> raise TypeError, "#{other.class} can't be coerced into TimeInterval"
> end
>
> would probably be better written as:
>
> case other
> when TimeInterval
> TimeInterval.new(value + other.value)
> when Numeric
> TimeInterval.new(value + other)
> else raise TypeError, "#{other.class} can't be coerced into
TimeInterval"
> end

Or at least:

if other.kind_of? TimeInterval
...
elsif other.kind_of? Numeric
...

Remember duck typing? I don't care if it is_a Numeric, not really. I can't
really think of many cases where is_a? makes more sense than kind_of?.

Disclaimer: The length of this post in particular has nothing whatsoever to do
with how strong or well-informed I am about the topic. I actually didn't know
about coercion until today, so I may be missing something obvious. Proceed
with caution...

> Or at least:
>
> if other.kind_of? TimeInterval
>
> ...
>
> elsif other.kind_of? Numeric
>
> ...
>
> Remember duck typing?

Good point,

Well, to a point. I just reflexively change is_a? to kind_of? everywhere I see
it, unless there's a good reason not to. I don't have a problem with your
cases, just pointing out an alternative.

but that said, it can be done even better:

First, in TimeInterval, alias_method :to_int, :value

If this article is correct:

http://www.rubyfleebie.com/to_i-vs-to_int/

...then I'm not sure that's the case. Do we always want to represent a time
interval as an integer number of seconds? Maybe, but my instinct is that maybe
there are other integers we could get out of this object.

if other.respond_to? :to_int
  TimeInterval.new(value + other.to_int)
elsif other.respond_to? :coerce
  a, b = other.coerce(self)
  a + b
else raise TypeError, "#{other.class} can't be coerced into TimeInterval"
end

While I agree that to_int is the correct way to check if something's an
integer, the original check was for anything numeric, so it might not be
sufficient (though it's probably fine for this class so far).

So maybe something like:

if other.respond_to?(:to_int) || other.kind_of?(Numeric)
  TimeInterval.new(value + other.to_int)
elsif other.respond_to? :coerce
  begin
    a, b = other.coerce(self)
    a + b
  rescue TypeError
    if other.respond_to?(:to_i)
      self + other.to_i
    else
      raise
    end
  end
elsif other.respond_to?(:to_i)
  self + other.to_i
else
  raise TypeError, "#{other.class} can't be coerced into TimeInterval"
end

That could probably be DRYed up a bit.

The idea is, if something has a to_int or is a Numeric of some sort, it's
claiming that it is _just_ a number, so we're probably free to treat it as
such. Otherwise, let it try to coerce first before we fall back to tricks like
to_i.

Why?

It's a contrived example to begin with, so my answer will be just as
contrived, but... Suppose I create, say, an HourInterval class, which
represents time intervals of hours. Then:

HourInterval.new(5)

You'd expect that to be five hours, right? Then you'd also expect:

HourInterval.new(5).to_i

...that should return 5. However, if we just let it to_i (or to_int, if it's
pretending to be just a number), then TimeInterval will treat it as five
_seconds_ instead of five hours, no matter what we do. That's why I'd suggest
giving it a chance to coerce first, and saving the fuzziest conversions for
when that fails.

Now the only trick is to make sure that we never call 'coerce' from inside a
'coerce' method, unless I'm missing something.

I'm told exception handling is expensive. I wonder if it might make sense to
have a non-raising form of 'coerce', or a can_coerce? method, to avoid that
overhead. But then, if you're doing this sort of thing and you start caring
about performance, you can always manually convert things ahead of time.

···

On Wednesday, January 26, 2011 10:52:25 pm Tony Arcieri wrote:

On Wed, Jan 26, 2011 at 9:20 PM, David Masover <ninja@slaphack.com> wrote:

Apparently so. Sorry for the noise...

···

On Thursday, January 27, 2011 10:19:18 am Adam Prescott wrote:

On Thu, Jan 27, 2011 at 4:20 AM, David Masover <ninja@slaphack.com> wrote:
> Remember duck typing? I don't care if it is_a Numeric, not really. I
> can't really think of many cases where is_a? makes more sense than
> kind_of?.

Am I missing something here? I don't see how this is in keeping with the
idea of duck typing: is_a? is an alias for kind_of? and vice-versa.

class Object - RDoc Documentation

Perhaps you were thinking of instance_of? here?