DEV Community

Cover image for Experiments in Overloading
Toby Inkster
Toby Inkster

Posted on • Originally published at toby.ink on

Experiments in Overloading

Let’s play with overloading a little.

A simple class:

  package Local::Overloaded {
    use Moo;

    has number => ( is => 'ro' );

    use overload '0+' => sub {
      my $self = shift;
      return $self->number;
    };
  }
Enter fullscreen mode Exit fullscreen mode

And let’s test it:

  use Test2::V0;
  my $obj = Local::Overloaded->new( number => 42 );
  is( 0+$obj, 42 );
  done_testing;
Enter fullscreen mode Exit fullscreen mode

This test fails.

Why?

We tend to think of 0+ as the way to “cast” a Perl variable to a number, so much so that the overload pragma even calls the numeric overload “0+”. However it is of course actually an addition, and we haven’t overloaded the plus operator.

The simple solution is to just including the fallback => true option when overloading. This tells Perl to fill in as many missing overloaded operations it can based on the operations you’ve explicitly provided.

But I’m not interested in the simple solution. I’m interested in what we can do with the “0+” overload.

Let’s try rephrasing our test case:

  use Test2::V0;
  my $obj = Local::Overloaded->new( number => 42 );
  ok( $obj == 42 );
  done_testing;
Enter fullscreen mode Exit fullscreen mode

This still fails. The equality operator isn’t overloaded either.

However, this one works:

  use Test2::V0;
  my $obj = Local::Overloaded->new( number => 42 );
  is( $obj, 42 );
  done_testing;
Enter fullscreen mode Exit fullscreen mode

So how is Test2 comparing them?

The answer is that whenever the right hand side of an is comparison is a non-reference, Test2 compares them as strings. Specifically, it does this:

  "$left" eq "$right"
Enter fullscreen mode Exit fullscreen mode

And for whatever reason, even though we didn’t set fallback => true, Perl will happily apply the numeric overload when an object is interpolated into a string and doesn’t have a string overload.

So let’s add some stringy overloading to our class:

  package Local::Overloaded {
    use Moo;
    use Lingua::EN::Numbers qw( num2en );

    has number => ( is => 'ro' );

    use overload (
      '0+' => sub {
        my $self = shift;
        return $self->number;
      },
      '""' => sub {
        my $self = shift;
        return num2en( $self->number );
      },
    );
}
Enter fullscreen mode Exit fullscreen mode

Now our previously passing test fails:

  use Test2::V0;
  my $obj = Local::Overloaded->new( number => 42 );
  is( $obj, 42 );
  done_testing;
Enter fullscreen mode Exit fullscreen mode

It is comparing “forty-two” and “42” as strings.

So this is where we stumble upon the safe way to cast to a number. And it’s still not 0+.

  use Test2::V0;
  my $obj = Local::Overloaded->new( number => 42 );
  is( sprintf( '%d', $obj ), 42 );
  is( sprintf( '%s', $obj ), 'forty-two' );
  done_testing;
Enter fullscreen mode Exit fullscreen mode

Passing the object to sprintf( '%d' ) for integers or sprintf( '%f' ) will actually use the numeric overload without complaining about == and + not being overloaded. This seems the most reliable way to cast an object to a number if it doesn’t have overload fallbacks enabled, or you’re not sure if it does.

The lesson here is that overloading can be weird in Perl, but using fallback => true makes it a lot less weird.

The other lesson is to use sprintf( '%d' ) or sprintf( '%f' ) to cast to a number instead of 0+.

Top comments (0)