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;
    };
  }

And let’s test it:

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

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;

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;

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"

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 );
      },
    );
  }

Now our previously passing test fails:

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

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;

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+.

One response to “Experiments in Overloading

Comments are closed.