This and other RFCs are available on the web at
  http://dev.perl.org/rfc/

=head1 TITLE

Objects : Core support for method delegation

=head1 VERSION

  Maintainer: Damian Conway <[EMAIL PROTECTED]>
  Date: 4 September 2000
  Mailing List: [EMAIL PROTECTED]
  Version: 1
  Number: 193
  Status: Developing

=head1 ABSTRACT

This RFC proposes that Perl 6 offer built-in support (via a pragma) for
delegating method calls to attributes of an object.


=head1 DESCRIPTION

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 => [],
                attr4 => [],
                # 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 in with an I<empty> 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 C<method6> method (or an C<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 C<AUTOLOAD> method.

Note that the presence of one or more catch-all's does not prevent an
C<AUTOLOAD> being called, if none of the catch-alls can handle the
requested method.

If the explicit methods, the catch-alls (and any C<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 C<use delegation> statement, with
all its delegation method lists being consolidated.

An attribute may also
appear with an explicit delegation list and as a catch-all. For example:

        use delegation
                attr1 => [qw(method1 method2)],
                attr2 => [qw(method3 method4)],
                attr1 => [],
                ;

This example specifies that calls to the methods C<method1> and
C<method2> should be delegated to the "attr1" attribute, calls to the
methods C<method3> and C<method4> should be delegated to the "attr2"
attribute, and any remaining calls that are not handled before the
C<AUTOLOAD> pass should be delegated to the "attr1" attribute (if
it can handle them).




=head2 New dispatch sequence

With delegation available, the method dispatch sequence becomes
(I<changes from Perl 5 mechanism in italics>):

=over 4

=item 1.

Look for the named method in current class, I<checking for explicitly
delegated methods as well>.

=item 2.

Recursively repeat steps 1 and 2 in ancestral class(es) and in UNIVERSAL.

=item 3.

I<Check for implicitly delegated methods through catch-all attributes.
These may be explicit or C<AUTOLOAD>ed in the attribute object's class.>

=item 4.

Check for C<AUTOLOAD> in current class

=item 5.

Recursively repeat steps 3 to 5 in ancestral class(es) and in UNIVERSAL.

=back


=head2 Using delegation instead of inheritance

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 Class::Delegation
                in  => [qw( getline getlines getc ungetc eof read sysread
                           input_record_separator input_line_number )],
                out => [],
                ;

        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 C<error> and
C<opened>, to catch cases where the method should be delegated to
I<both> attributes.


=head2 Replacing multiple inheritance

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 => [], ancestor2 => []);

        sub new {
                my PseudoMI $self = fields::new($_[0]);
                $self->{ancestor1} = PseudoBase1->new();
                $self->{ancestor2} = PseudoBase2->new();
                return $self;
        }


=head2 Delegation to avoid inheritance

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 => [];

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


=head2 Delegation for sanitization

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 => [];


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.


=head1 MIGRATION

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.



=head1 IMPLEMENTATION

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.


=head1 REFERENCES

RFC 137: Overview : Perl OO should I<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.

Reply via email to