Telephone +44(0)1524 64544
Email: info@shadowcat.co.uk

Madness With Methods

The beauty and insanity of perl5 method call semantics

Mon May 4 22:10:00 2009

Digg! submit to reddit Delicious RSS Feed Readers

Madness With Methods

Note: for those of you wondering about the status of various Iron Man things, please see the short updatelet at the bottom

And now, onwards.

In this post, I'm going to try and explain some of the crazier things you can do with perl5 method dispatch. So, for illustrative purposes, first we set up a quick package -

  use strict;
  use warnings;
  {
    package MethodMadness;
    sub foo {
      "foo on a ".ref($_[0]);
    }
  }

Then make an object of that package (yes, yes, we should have a constructor, but a quick bless to create an object is good enough for the purpose)

  my $obj = bless({}, 'MethodMadness');

Now, a simple method call: you all know this one, right?

  $obj->foo; # returns "foo on a MethodMadness"

Or we can get the method name from a variable:

  my $meth = "foo";
  $obj->$meth; # also returns "foo on a MethodMadness"

But, actually, it doesn't have to be a name - you can invoke a subroutine reference this way too:

  my $bar = sub { "bar on a ".ref($_[0]) };
  $obj->$bar; # returns "bar on a MethodMadness"

Now, of course, it's actually pretty equivalent to:

  sub { "baz on a ".ref($_[0]) }->($obj); # returns "baz on a MethodMadness"

Which makes you wonder ... what can come after the $ sign - it's something that's producing a scalar so ... what about other ways of getting a scalar out of that? Well:

  $obj->${\sub { "baz on a ".ref($_[0]) }}; # returns "baz on a MethodMadness"

So what's going on here? Well, ${...} is a scalar dereference - ${$foo} is equivalent to $$foo, so if $foo = \$bar then ${$foo} is $bar. Got it?

Here's the point: \sub creates a subref-*ref*, which the ${} then turns back into a subref, which reduces it to the above $obj->$bar case - and thus works.

And the same trick also works with strings:

  my $meth = "fo";
  $obj->${\"${meth}o"}; # returns "foo on a MethodMadness"

Now, of course, the inline forms are ... kinda ugly. I mean, ``reasons to hate perl'' ugly, or at least verging on it.

Short detour before we fix that:

  sub double { $_[0]*2 }
  "twice 2 is ${\double(2)}"; # returns "twice 2 is 4"

can come in handy when you have a small complex interpolation and don't really want to break out to concatenation. It's also a really important reason why you can never eval a string input from a user, even if you think there's no variable in scope it'd be bad for them to get (String::TT is your frend).

Going back to method calling, the interesting thing about the ${} syntax is that -any- valid perl expression can live within there. So anything that produces a scalarref or a subref-ref is valid. So:

  sub maybe {
    my $meth = shift;
    \sub {
      $_[0]->can($meth)
        ? shift->$meth(@_)
        : undef
    }
  }
  my $obj2 = bless({}, 'NoMethodsAtAll');
  $obj->${maybe 'foo'};  # returns "foo on a MethodMadness"
  $obj2->${maybe 'foo'}; # returns undef

Which is kind of handy if you're trying to call a getter and don't care if it isn't there. Certainly it's nicer than the more naive

  sub if_method {
    my ($obj, $meth) = @_;
    if ($obj->can($meth)) {
      $obj->$meth;
    } else {
      undef;
    }
  }
  if_method($obj, 'foo');  # returns "foo on a MethodMadness"
  if_method($obj2, 'foo'); # returns undef

Of course, we can clean this up -slightly-, or at least, make it slightly more efficient, by changing

  if ($obj->can($meth)) {
    $obj->$meth;
  }

to

  if (my $cr = $obj->can($meth)) {
    $obj->$cr;
  }

but it's all the same from an interface perspective. The thing is, though, the efficiency gain is fairly irrelevant; I prefer the latter in a lot of cases because it's an expression of intent, and because the usage remains similar if you want to use e.g. the C3 next::can (next::method and next::can are generally way more useful than SUPER if multiple inheritance might be involved, though that's a whole different article).

Of course, in the case of maybe() most of these things don't apply and I think the code is clearer as I wrote it - and given I strongly suspect that the sub call overhead of using maybe at all means efficiency within it is fairly irrelevant. If you ever profile an app using this trick and discover that not only am I wrong but it makes a noticeable difference to our performance, mail me and I'll update this paragraph :)

Getting back to code, maybe is cute but not -that- useful - it handles when an object exists but doesn't have the right method, but the more common case for this sort of thing is:

  if (my $foo = $obj->foo) {
    if (my $bar = $foo->bar) {
      if (my $baz = $bar->baz) {
        return $baz;
      }
    }
  }
  return undef;

and of course since maybe currently does $obj->can($meth) this is going to blow up when called on undef. So the first thing is to give maybe a smarter can to use:

  my $can = sub { Scalar::Util::blessed($_[0]) && $_[0]->can($meth) };
  sub maybe {
    my $meth = shift;
    \sub {
      $_[0]->$can($meth)
        ? shift->$meth(@_)
        : undef
    }
  }

which works because of an interesting property:

  my $undef = undef;
  my $code = sub { "foo" };
  $undef->$code; # returns "foo"

i.e. when given a coderef rather than a method name, method lookup is not invoked at all, and it's actually the lookup process that complains if the invocant isn't an object (this is also why autobox can work, it simply wraps the stuff that does the looking up part).

So now we can write

  $obj->${maybe 'foo'}->${maybe 'bar'}->${maybe 'baz'};

to replace the nested if stuff above. Of course, that's still quite verbose - it'd be really nice if we could chain calls together. Well ... actually, that's not that hard to do:

  sub chain {
    my $first = shift;
    if (my @args = @_) {
      my $rest = chain(@args);
      return sub { $_[0]->$first->$rest };
    } else {
      return sub { $_[0]->$first };
    }
  }
  my $can = sub { Scalar::Util::blessed($_[0]) && $_[0]->can($meth) };
  my $builder = sub {
    my $meth = shift;
    sub {
      $_[0]->$can($meth)
        ? shift->$meth(@_)
        : undef
    };
  };
  sub maybe {
    \chain(map { $builder->($_) } @_);
  }
  $obj->${maybe qw(foo bar baz)};

And finally, let's seriously pretty it up with the aid of some extra modules:

  use Method::Signatures::Simple;
  use signatures;
  sub chain ($first, @rest) {
    if (@rest) {
      my $rest = chain(@rest);
      return method { $self->$first->$rest };
    } else {
      return method { $self->$first };
    }
  }
  my $can = method { Scalar::Util::blessed($self) && $self->can($meth) };
  sub builder ($meth) {
    method (@args) {
      $self->$can($meth)
        ? $self->$meth(@args)
        : undef
    };
  };
  sub maybe {
    \chain(map { $builder->($_) } @_);
  }
  use namespace::clean; # so the subs are cleaned out and don't show up as methods
  $obj->${maybe qw(foo bar baz)};

Presto! Welcome to perl5 - sufficiently advanced insanity :)

Comments

No, this blog doesn't have a comments system. Yes, lack of comments sucks. Yes, we should publish the damn codebase this site runs on so people can add it for us or something. No, I don't have time to do that yet. So, here's the compromise - mail mst-comments at shadowcat.co.uk with your comment, including the URL of the post you're commenting on in the subject, and your comment as either plain text, or as HTML inlined in the body of a plain text email (because I use mutt and I delete anything HTML only that doesn't come from a client :), and I'll add them to the bottom of the post by hand until it gets annoying enough to do something more sensible.

Iron Man Updatelet

Well, the planet is currently on its way from Dreamhost to a Shadowcat server - for all the whining about them they're great bang for the buck if you want cheap and simple, but we've had so many entrants that the planet code was a little bit on the heavy side for them, so we've moved it.

Once that's done, we'll be able to get feed post processing and the badges and stuff together - I'll be publishing the repository of the code that does that so people can find it and fix my crappy attempt at perl :)

I'd like to say "OH MY GOD WOW", by the way. I hoped we'd manage to get a bunch of people up and talking, but the response has been awesome. You should all be fucking proud of yourselves. You should all also try and recruit one more person, because if even a fifth of you manage that we're going to pass a hundred signups in the first month - and that would just be fucking unbelievable!

I also need to take a moment to thank the denizens of #epo-ironman on irc.perl.org who've been toiling tirelessly to make this thing work - it's not perfect yet but software being hateful as always there's been a lot of effort put in to making it work as well as it does - miyagawa, the author of the Plagger feed processor and aggregator software that powers the planet has even been kind enough to turn up and give us a hand.

Finally, while I'd seriously encourage you to all go and set up your own blogs so that we show that the perl community isn't just a handful of sites, if you don't have the time or the means or the whatever to do so, the guys over at Catalyzed have been kind enough to offer an account to anybody who wants to join Iron Man and otherwise can't, so if that's all that's stopping you go fucking hit them up right now!

mst, over and out. Keep fucking going people, this is AWESOMELY COOL