[% setvar title Objects : Core support for method delegation %]
Note: these documents may be out of date. Do not use as reference! |
To see what is currently happening visit http://www.perl6.org/
Objects : Core support for method delegation
Maintainer: Damian Conway <damian@conway.org> Date: 4 Sep 2000 Last Modified: 25 Sep 2000 Mailing List: perl6-language-objects@perl.org Number: 193 Version: 2 Status: Frozen
This RFC proposes that Perl 6 offer built-in support (via a pragma) for delegating method calls to attributes of an object.
Delegation of method calls to attributes is a powerful OO technique that is not well supported in most OO-capable languages, including Perl 5.
Delegation offers most of the advantages of inheritance (and, more particularly, multiple inheritance) without most of the headaches. It also offers some extra features that inheritance cannot provide.
The proposed delegation mechanism would work via a pragma:
use delegation attr1 => [qw( method1 method2 method3 )], attr2 => [qw( method4 method5 )], attr3 => __ALL__, attr4 => __ALL__, # etc. ;
This would cause method calls whose names match an element in the first list to be delegated to the "attr1" attribute of an object. Likewise, calls to a method whose name appears in the second list would be forwarded to the "attr2" attribute of the object.
That is, calls like:
$obj->method3(@args); $obj->method5(@other_args);
would act as if they were:
$obj->{attr1}->method3(@args); $obj->{attr2}->method5(@other_args);
(and, if these attribute objects also delegated, the process might repeat recursively until some deeply nested attribute actually provided a method to call).
Attributes which appear with the string "__ALL__" instead of a method list become "catch-alls". Unresolvable method calls are delegated to the first of these that is able to handle it.
So, for example, a call like:
$obj->method6(@more_args);
would become equivalent to:
$obj->{attr3}->method6(@more_args);
if $obj->{attr3} had a method6
method (or an AUTOLOAD
), or else:
$obj->{attr4}->method6(@more_args);
if $obj->{attr4} had a suitable method.
Unlike explicitly delegated methods, which are delegated on the first
method look-up pass, delegation to catch-alls occurs on the second pass,
just before the dispatch mechanism tries the package's AUTOLOAD
method.
Note that the presence of one or more catch-all's does not prevent an
AUTOLOAD
being called, if none of the catch-alls can handle the
requested method.
If the explicit methods, the catch-alls (and any AUTOLOAD
) all fail to
provide a suitable method, the normal dispatch would then continue into the
object's ancestral classes (if any).
An attribute can appear several times in a use delegation
statement, with
all its delegation method lists being consolidated.
An attribute may also appear both with an explicit delegation list and as a catch-all. For example:
use delegation attr1 => [qw(method1 method2)], attr2 => [qw(method3 method4)], attr1 => __ALL__, ;
This example specifies that calls to the methods method1
and
method2
should be delegated to the "attr1" attribute, calls to the
methods method3
and method4
should be delegated to the "attr2"
attribute, and any remaining calls that are not handled before the
AUTOLOAD
pass should be delegated to the "attr1" attribute (if
it can handle them).
With delegation available, the method dispatch sequence becomes (changes from Perl 5 mechanism in italics):
Look for the named method in current class, checking for explicitly delegated methods as well.
Recursively repeat steps 1 and 2 in ancestral class(es) and in UNIVERSAL.
Check for implicitly delegated methods through catch-all attributes.
These may be explicit or AUTOLOAD
ed in the attribute object's class.
Check for AUTOLOAD
in current class
Recursively repeat steps 3 to 5 in ancestral class(es) and in UNIVERSAL.
One powerful application of delegation is as a replacement for inheritance where the internals of a prospective base class are inaccessible or inconvenient, or the base class was not designed to be inherited and yet it must be.
For example, consider the task of creating an IO::File-like class that reads and writes to separate handles:
use IO::Bi; my $handle = IO::Bi->new('infile', 'outfile'); if (defined($_ = $handle->getline)) { $handle->print($_); } foreach ($handle->getlines) { $handle->print($_); }
IO::Bi can't inherit from IO::File, because it needs two file handles, with input methods going to one and output methods going to the other. That's impossible with inheritance (even using the dreaded "diamond inheritance pattern") because a class can inherit the state of any ancestral class only once, no matter how many paths that ancestor is inherited through. In C++ terms, all inheritance in Perl is "virtual".
With delegation, the solution is trivial:
package IO::Bi; use IO::File; sub new { my ($class, $infile, $outfile) = @_; bless { in => IO::File->new($infile) || die, out => IO::File->new("> $outfile") || die, }, $class; } use delegation in => [qw( getline getlines getc ungetc eof read sysread input_record_separator input_line_number )], out => __ALL__, ; sub error { my ($self) = @_; return $self->{in}->error || $self->{out}->error; } sub opened { my ($self) = @_; return $self->{in}->opened && $self->{out}->opened; }
Here, all the input-specific methods are passed to the filehandle stored in the "in" attribute. Everything else goes to the "out" attribute's filehandle.
Note that the class can still define explicit methods like error
and
opened
, to catch cases where the method should be delegated to
both attributes.
Another situation where delegation is useful is where it is necessary to inherit from two classes that are both implemented via pseudohashes. Multiple inheritance of pseudohashes is impossible (for good reasons), but delegation gives almost the same effect:
package PseudoMI; use fields qw(ancestor1 ancestor2); use delegation (ancestor1 => __ALL__, ancestor2 => __ALL__); sub new { my PseudoMI $self = fields::new($_[0]); $self->{ancestor1} = PseudoBase1->new(); $self->{ancestor2} = PseudoBase2->new(); return $self; }
Delegation is also powerful where inheritance is possible, but inconvenient because the base class is not designed to be inherited, or inheritance semantics (such as automagic base class dtor calls) are undesirable:
package File::Lock::Mac; use File::Lock; use delegation uninherited_but_used => __ALL__; sub new { my $class = shift @_; bless { uninherited_but_used => File::Lock->new(@_) }, $class; } use Mac::Files; sub lock { my ($self) = @_; Mac::Files::FSpSetFLock($self->filename); } sub DESTROY { my ($self) = @_; Mac::Files::FSpRstFLock($self->filename); }
Delegation also provides a mechanism for upgrading legacy code to take advantage of new OO features. Consider the encapsulated (private) hash entries proposed by RFC 188. These provide a simple mechanism for strong encapsulation but are only applicable to hashes.
But, with delegation, a legacy base class could be integrated into a Perl 6 class that is based on encapsulated hashes, regardless of how the legacy class itself is implemented.
Consider extending a Person class (with objects implemented as, say, blessed subroutines) with dog-tag information (to be implemented using privatized hashes):
package DogTag; use Person; sub new { my ($class, $name, $rank, $snum) = @_; bless private { name => Person->new($name), rank => $rank, snum => $snum, }, $class; } sub rank { my ($self, $newval) = @_; $self->{rank} = $newval if @_ > 1; return $self->{rank}; } sub serial_num { my ($self, $newval) = @_; $self->{snum} = $newval if @_ > 1; return $self->{snum}; } use delegation name => __ALL__;
Note that the DogTag class is completely insulated from the implementation details of the Person class. The above code would be identical if Person objects were hashes, arrays, pseudohashes, flyweight scalars, blessed regexes or typeglobs, or even some arcane XS-generated C struct.
Yet from outside the DogTag class, the "name" data is encapsulated in exactly the same way as the rank and serial number information. And the methods of the Person class are directly usable on DogTag objects, just as if they had been inherited.
It is envisaged that delegation -- particularly the "sanitization" technique shown in the last example above -- would provide an important bridging mechanism for migrating existing Perl 5 OO class hierarchies to the new features and functionalities of Perl 6.
Of itself, the pragma would break no existing code.
A module named Class::Delegation will soon appear on the CPAN.
It implements almost this entire proposal, except that it is forced to use $self->SUPER::AUTOLOAD(), rather than $self->NEXT::AUTOLOAD(), in cases where delegation fails. This prevents the module from achieving the full transparency that the pragma would offer.
The module's delegation speed is also very slow compared with what an built-in pragma would allow the interpreter to achieve.
RFC 137: Overview : Perl OO should not be fundamentally changed.
RFC 188: Objects : Private keys and methods
RFC 189: Objects : Hierarchical calls to initializers and destructors
RFC 190: Objects : NEXT pseudoclass for method redispatch
Sean M. Burke's Class::Classless module.
The Self progamming language <www.objs.com>
The Objective-C progamming language
The Cecil progamming language
The Scheme progamming language
The Smalltalk-80 progamming language
www-xdiv.lanl.gov#S-3.2
Lynn Andea Stein, "Delegation is Inhertance", Proc. OOPSLA '87, Orlando, FL, October 4-8, 1977, pp. 138-146