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.
Let's take this method call chain:
we want an object $chain so that $object->$chain does the same as the above method call.$object->method1(@args1)->method2(@args2)->method3(@args3)
For the sake of easy interface, we'll construct this with the simple syntax
$chain = Object::MethodChain->method1(@args1)->method2(@args2)->method +3(@args3);
$chain = Object::MethodChain->new("method1", \@args1, "method2", \@arg +s2, "method3");
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
so$chain = Object::MethodCall->method1(@args1)->method2(@args2);
has to be an object, so you can't use a string. The way around this is overloading: use an object that stringifies magically.Object::MethodCall->method1(@args1)
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:
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:$chain = Object::MethodChain->foo;
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.
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; }
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
does the same as above. In this case, you get a simple flattened method chain. $c Dumpered from the above is{ 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; }
bless( [ { 'args' => [], 'method' => 'new' }, { 'args' => [], 'method' => 'foo' }, { 'args' => [ undef ], 'method' => 'bar' }, { 'args' => [ 'her p' ], 'method' => 'baz' } ], 'Object::MethodChain' );
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 | |
by ambrus (Abbot) on Apr 19, 2005 at 20:38 UTC | |
by ihb (Deacon) on Apr 20, 2005 at 11:33 UTC | |
by ambrus (Abbot) on Apr 20, 2005 at 20:05 UTC | |
|
Re: Method chain object with easy syntax
by japhy (Canon) on Apr 16, 2005 at 16:42 UTC | |
|
Re: Method chain object with easy syntax
by simonm (Vicar) on Apr 16, 2005 at 21:24 UTC | |
by jdporter (Paladin) on Apr 17, 2005 at 17:43 UTC | |
|
Re: Method chain object with easy syntax
by dmitri (Priest) on May 24, 2014 at 15:06 UTC |