Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

Method chain object with easy syntax

by ambrus (Abbot)
on Apr 16, 2005 at 11:02 UTC ( #448444=perlmeditation: print w/replies, xml ) Need Help??

Introduction

During a discussion on the cb, with jZed, bart has mentioned that it would be useful to represent chains of methods like the ->foo->bar part of $object->foo->bar; as one entity. In this meditation, I show that this is possible in perl.

Goals

Let's take this method call chain:

$object->method1(@args1)->method2(@args2)->method3(@args3)
we want an object $chain so that $object->$chain does the same as the above method call.

For the sake of easy interface, we'll construct this with the simple syntax

$chain = Object::MethodChain->method1(@args1)->method2(@args2)->method +3(@args3);
This complicates out implementation a bit, because we have to use AUTOLOAD. We could avoid AUTOLOAD and gain more flexibility if we used some mode complicated syntax such as
$chain = Object::MethodChain->new("method1", \@args1, "method2", \@arg +s2, "method3");

Implementations

I'll show multiple versions of the code.

The tricky part of the code is how we create an object $chain dynamically in such a way that $class->$chain does something useful. We could avoid this if we allowed for another compromise in the interface, like requiring $chain->call_on($object) instead of $object->$chain. You might find this trivial, but this is the real reason I wrote this meditation.

To solve this, let's see what the $object->$method syntax does. If $method is a string without a double colon, then this is a real method call which calls the method named $method from the class of $object or from the class $object if $object is a string. (There's also the same magic for filehandles as with the normal ->method method calls.) We don't know what class will be used, so we can't want to install a new method in it. However, we can install a method in UNIVERSAL. But there's another problem too: we want to be able to do

$chain = Object::MethodCall->method1(@args1)->method2(@args2);
so
Object::MethodCall->method1(@args1)
has to be an object, so you can't use a string. The way around this is overloading: use an object that stringifies magically.

I shall create only one method in UNIVERSAL, and use a global variable to pass the method chain to that method. This is because if we created a new method for every Object::MethodChain ever stringified, those methods could not be garbage collected. (Also, it would probably be slow because of the method cache, but I'm not sure in this.) Thus, the stringification method will return a constant string, the name of this method, but it will store the data about the method chain in a global variable.

Here's the code. (Assume use warnings; use strict; for here and the rest of the writeup.)

{ package Object::MethodChain; use overload q[""], "__Set_MethodChain__"; AUTOLOAD { my $self = shift; my $class = ref($self) || $self; my @new = ref($self) ? @$self : (); our $AUTOLOAD =~ /.*::(.*)/s or die "error: invalid method name"; push @new, {"method", $1, "args", \@_}; bless \@new, $class; } our $chain; sub __Set_MethodChain__ { $chain = $_[0]; "__Call_MethodChain__"; } sub UNIVERSAL::__Call_MethodChain__ { my $r = $_[0]; for my $pair (@$chain) { my($method, $args) = @$pair{"method", "args"}; $r = $r->$method(@$args); } $r; } DESTROY { } }

There's one more bit you should note: we store \@_ so that lvalue arguments to methods work.

Let's continue thinking what $method could me in a $object->$method call. If $method is a string with double colons, or a code reference, or a glob, then $object->$method is equivalent to &$method($object).

Code reference is a good way to go, as we can easily create a code reference with whatever content we want, and it can be blessed too. This has the disadvantage that you can't print the chain object with Data::Dumper (you can with the above).

The code is much shorter then the first one, in fact, this was my first version of the code:

{ package Object::MethodChain; AUTOLOAD { my $s = shift; my $c = ref($s) || $s; my $p = ref($s) ? $s : sub { $_[0] }; our $AUTOLOAD =~ /.*::(.*)/s or die; my $m = $1; my $a = \@_; bless sub { &$p($_[0])->$m(@$a); }, $c; } DESTROY { } }

The above code does not use an array to store the methods and their arguments, instead, it creates a sequence of closures each of which reference the previous one through an enclosed variable.

If you don't understand this code, first consider the case when there's only one method in a chain. This call:

$chain = Object::MethodChain->foo;
will call Object::MethodChain->AUTOLOAD with $Object::MethodChain::AUTOLOAD set to "Object::MethodChain::foo. The AUTOLOAD function then sets $m to "foo", $p to sub { $_[0] }, the identity function, and $a to a reference to an empty array. The (blessed) code reference it returns has these variables enclosed. The code is sub { &$p($_[0])->$m(@$a); } which, when substituted the variables, becomes sub { &{sub { $_[0] }}($_[0])->"foo"(@$a); }, which is roughly equivalent to this:
sub { $_[0]->foo; }

Then, if we call $object->$chain, as $chain is a sub, it calls &$chain($object) which does $object->foo.

Now you can probably find out yourself how the chaining case works: the variable $p stores the Object::MethodChain object for the methods before the last one.

I show a more straightforward solution here which stores the data in an array like in the first solution. This also has the advantage that it doesn't go to a deep recursion if you use a very long method chain. (Actually, you could use a closure chain in the overloading solution too, but an array seemed more natural.)

For this, we need a way to get the array from such an object. My solution to this is to pass a special argument to the sub. We could just as well use some other way, like setting a global variable.

{ package Object::MethodChain; our $open_sesame = []; AUTOLOAD { my $self = shift; my $class = ref($self) || $self; my @chain = ref($self) ? &$self($open_sesame) : (); our $AUTOLOAD =~ /.*::(.*)/s or die "error: invalid method name"; push @chain, {"method", $1, "args", \@_}; bless sub { if(ref($_[0]) && $open_sesame == $_[0]) { @chain; } else { my $r = $_[0]; for my $pair (@chain) { my($method, $args) = @$pair{"method", "args"}; $r = $r->$method(@$args); } $r; } }, $class; } DESTROY { } }

But let's get back to what I've said: $method could also be a fully qualified subname, or a glob. I can't solve this with a glob, as that can't be blessed. However, we can use an object that overloadingly stringifies to a fully qualified name. That gives another solution that's almost the same as the first one:

{ package Object::MethodChain; use overload q[""], "__Set_MethodChain__"; AUTOLOAD { my $self = shift; my $class = ref($self) || $self; my @new = ref($self) ? @$self : (); our $AUTOLOAD =~ /.*::(.*)/s or die "error: invalid method name"; push @new, {"method", $1, "args", \@_}; bless \@new, $class; } our $chain; sub __Set_MethodChain__ { $chain = $_[0]; "Object::MethodChain::__Call_MethodChain__"; } sub __Call_MethodChain__ { my $r = $_[0]; for my $pair (@$chain) { my($method, $args) = @$pair{"method", "args"}; $r = $r->$method(@$args); } $r; } DESTROY { } }

Note finally that you cannot use an object with sub dereferencing (&{}) overloaded, as $object->$method always calls the stringification overload of $method instead, and doesn't like if the stringification function returns a coderef.

Example

This Object::MethodChain object can be called on an object or a package name bareword or a filehandle, just like any ordinary method. Needless to say that MethodChain works even if a method returns something else than the original object, the next method will be called on the next object. Here's an example of using MethodChain. The following example is intended to show all of the above features, and it also shows that you can call a method of Object::MethodChain indirectly (see below ->$f where $f = "foo").

The example works with any of the following definitions. Take the following definitions:

{ package AnObj; sub new { bless [], $_[0]; } sub foo { print "just "; $_[0]; } sub bar { $_[1] = "ack"; OtherObj->new("erl h"); } } { package OtherObj; sub new { bless [$_[1]], $_[0]; } sub baz { print "anot", $_[1], $_[0][0]; "er,\n"; } }

Then the following chain method call prints a familiar message:

{ my $f = "foo"; my $n = AnObj->new->$f->bar(my $v)->baz("her p"); print $v, $n; }

We can substitute the chain with a single call of a method chain object, and the results are the same:

{ my $f = "foo"; my $c = Object::MethodChain->new->$f->bar(my $v)->baz("her p"); my $n = AnObj->$c; print $v, $n; }

Limits

You cannot use the can or isa UNIVERSAL methods on a method chain. While you can use an indirect method call Object::MethodChain->$f if $f is a name of a method (without double colons), you usually cannot use a fully qualified indirect method call, i.e. when $f is a subname with package prefix or a sub reference.

As an interesting exception however, you can use an Object::MethodChain as a method to call on another Object::MethodChain (or the class itself). For example this code

{ my $f = "foo"; my $m = Object::MethodChain->$f->bar(my $v); my $c = Object::MethodChain->new->$m->baz("her p"); my $n = AnObj->$c; print $v, $n; }
does the same as above. In this case, you get a simple flattened method chain. $c Dumpered from the above is
bless( [ { 'args' => [], 'method' => 'new' }, { 'args' => [], 'method' => 'foo' }, { 'args' => [ undef ], 'method' => 'bar' }, { 'args' => [ 'her p' ], 'method' => 'baz' } ], 'Object::MethodChain' );

Thoughts

I can't help noticing the similarity between this class and the __ object in the Switch package. It would probably even possible to combine them.

Update: agressive shortdown with readmore tags.

Update: changed the details of the cb conversation I didn't remember correctly.

Update: there's a bug in three of the implementations of the method chain, which causes a method chain call not to propagate context, and always call the last method in scalar context. See the discussion in the replies.

Replies are listed 'Best First'.
Re: Method chain object with easy syntax
by ihb (Deacon) on Apr 16, 2005 at 21:54 UTC

    I must be missing something. What's wrong with doing

    $chain = sub { $_[0]->method1(@args1)->method2(@args2)->method3(@args3 +) };
    ? It doesn't suffer for the limitations you decribe. The only limitation as far as I can see is that it creates another scope if you use my() et al in the arguments, which is a bad idea anyway and is easily worked around.

    Anyway... About your post:

    If $method is a string with double colons, or a code reference, or a glob, then $object->$method is equivalent to &$method($object).

    It's not true if it's a string with double colons or a glob. It searches the inheritance tree starting at the package specified in the string. The glob stringifies and for some reason '*Bar::foo' is also accepted.

    #!/usr/bin/perl -wl use strict; no strict 'refs'; { package Foo; sub foo { 1 } } { package Bar; use base 'Foo'; } my $object = 'Bar'; # just the class name my $method = 'Bar::foo'; print "$object->$method ", eval { $object->$method } ? 'ok' : 'not o +k'; print "&$method($object) ", eval { &$method($object) } ? 'ok' : 'not o +k'; __END__ Bar->Bar::foo ok &Bar::foo(Bar) not ok
    Btw, I think you're playing with fire when you do
    for (@$chain) { ... }
    since $chain @$chain may change during the loop. I'd feel much safer if you made a local copy of the array.

    ihb

    See perltoc if you don't know which perldoc to read!

      I don't know any reasons why this class can be useful in real life (i.e. production code). I wrote it more for learning perl then for actuall using it. Anyway, as I've mentioned, this technique could be useful for extending Switch or Quantum::Superpositions.

      As for the semantics of $o->$m, you are right. It does inherit, and it does accept a stringified glob.

      Btw, I think you're playing with fire when you do
      for (@$chain) { ... }
      since $chain may change during the loop. I'd feel much safer if you made a local copy of the array.

      I disagree here. While the scalar $chain can change, it can point to a different array later, the scalar itself is fetched only once. The important point is that the contents of the array it points to at start doesn't change, and that is true.

      Also, it is easy to verify that the implementation is reentrant with a code like this:

      Update: In the simple case, sub { $_[0]->m1(@a1)->m2(@a2) } is of course simpler, but if you want to build a chain of methods without the length of it known in advance, then the sub solution can be more difficult.

        the scalar itself is fetched only once

        Is that guarenteed? If it is I almost withdraw my opinion. I say almost, because I still think it's unnecessary complicated (in that it uses a global as loop variable). If you change

        for my $pair (@$chain) {
        to the not completely unusual construct
        while (@$chain) { my $pair = shift @$chain;
        for whatever reason it'll demonstrate why I think this is playing with fire -- you have an extra thing to keep in mind two years later when you patch the code. As you say yourself
        The important point is that the contents of the array it points to at start doesn't change
        These's a general concensus that using globals like this is A Bad Thing, even if it's not such an obvious case. Not only do you have to remember that directly inside the loop not change @$chain, but you can't get the idea to assign to @$chain in &__Set_MethodChain__. That's a lot of unnecessary conditions just to not copy a presumably small array. If you copy the array you don't have anything extra to worry about and that might save yourself from future troubles. Indeed, the code works, I just get a bad feeling when I see it. :-)

        if you want to build a chain of methods without the length of it known in advance, then the sub solution can be more difficult

        You can easily compose new chains using other "sub chains", if that's what you're talking about.

        #!/usr/bin/perl -wl AUTOLOAD { print $::AUTOLOAD =~ /.*::(.*)/s; $_[0]; } my $chain1 = sub { $_[0]->m2->m3 }; my $chain2 = sub { $_[0]->m1->$chain1->m4->m5 }; main::->$chain2; __END__ m1 m2 m3 m4 m5
        Maybe I misunderstood?

        ihb

        See perltoc if you don't know which perldoc to read!

Re: Method chain object with easy syntax
by japhy (Canon) on Apr 16, 2005 at 16:42 UTC
    Update: I apologize; I was too hasty and did not read the node's contents fully. I see what you did, and what you needed to do it that way.

    What makes you think you need to overload stringification? If you do:

    my $obj = Class->new; my $func = $obj->can('some_method');
    then $func is a code reference, and you can do
    $obj->$func(...);
    You don't need $func to be a string.
    _____________________________________________________
    Jeff japhy Pinyan, P.L., P.M., P.O.D, X.S.: Perl, regex, and perl hacker
    How can we ever be the sold short or the cheated, we who for every service have long ago been overpaid? ~~ Meister Eckhart
Re: Method chain object with easy syntax
by simonm (Vicar) on Apr 16, 2005 at 21:24 UTC
    Can some of the cb discussion participants draw out the use case for having the chain stored as a separate object, rather than as data?
    $object->Method::Chain(method1=>\@args1, method2=>\@args2);
      Among other things, we'd like to be able to call the method chain for different objects. One way of thinking of this is like function composition, but for methods.
Re: Method chain object with easy syntax
by dmitri (Priest) on May 24, 2014 at 15:06 UTC
    Thank you for this article. It gave me several ideas, which will now stew for a while in my brain :)

    Why not make Object::MethodChain a CPAN module? I think it's worthy of it.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://448444]
Approved by Arunbear
Front-paged by demerphq
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others cooling their heels in the Monastery: (5)
As of 2021-05-09 20:38 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    Perl 7 will be out ...





    Results (102 votes). Check out past polls.

    Notices?