Using AUTOLOAD for object proxy and method resolution - Does Moose offer a better alternative?

I've written a couple articles on encapsulation issues in perl OO, especially with regards to home-grown versus full-framework OO.

I am told that finding my own solutions to get the behavior I want may lead to poor (dirty, unreliable) code when a better solution could come through a framework such as Moose.

I rolled my own perl OO for Nama, a multitrack audio recording app I've written. However, I'm ready to consider introducing an OO framework for the sake of learning, code quality, testing and maintenance. Provided that I can find satisfactory ways to solve my problems.

Here's a laundry list of (just two) OO features that I'd need or want to implement in a Mooselike framework. One is short and simple to describe, the other will take several paragraphs.

Inheriting field definitions

Track objects in Nama have 34 attributes, and are the biggest class by far. Track subclasses serve special purposes that somewhat evident in their names: SimpleTrack, CacheRecTrack, MixDownTrack, EditTrack, VersionTrack, MixTrack. I've found it convenient to write the base class so Track subclasses inherit attribute names from ancestor classes.

Update: I see this is default behavior for Moose

Object proxy and attribute overriding

I'd read about AUTOLOAD for years, and wondered what the fuss was about. With my home-grown OO, I found I needed to use it.

Here is what I did.

Problem domain - generating signal processing networks

A lot of the complexity in multitrack audio production, relates to routing, that is, creating signal processing networks. Signals travel

  1. from sources such as audio files, soundcards or programs
  2. through various components such as tracks and buses,
  3. into sinks such as audio files, soundcards or programs

Each recording or playback run requires an appropriate network. Nama doesn't perform audio processing itself; it invokes the Ecasound audio engine, expressing the signal-processing network as an Ecasound chain setup file.

Chain setups are made up of one or more signal chains. Each chain has one input and one output. Intermediate nodes called loop devices allow for summing or branching. Whenever needed or commanded by the user, Nama iterates over all tracks and generates a routing graph. The graph is implemented as a Graph object whose nodes and edges may contain attributes.

Nama traverses this graph to generate a set of IO objects, each corresponding to the input or output clause of an Ecasound chain. IO subclasses correspond to each variety of input or output. To give one example, an IO::wav_out object is used for writing a signal to a WAV file. It creates a line such as:

 -a:2 -f:s16_le,2,44100 -o:Mixdown_2.wav

where "2" is the chain's ID, "s16_le,2,44100" is the signal format, and "Mixdown_2.wav" is the output file.

Iterating over the IO objects generates the entire Ecasound chain setup.

Generally attributes of the Graph node or edge will override the methods of the corresponding IO object, but in some cases methods have priority.

If an IO object doesn't have a particular attribute that one of its methods needs, it looks to the corresponding Track object (proxy relationship.)

AUTOLOAD implementation described

The way I currently implement this is as follows: When constructing IO objects, each key gets an appended underscore ("chain_id" becomes "chain_id_".)

Within the class definition, methods can be prefixed by an underscore to allow them to be overridden.

package IO::wav_out;
sub _format_template { $raw_to_disk_format }

When resolving $io_object->format_template, since format_template is not defined, the call is picked up by the AUTOLOAD subroutine.

AUTOLOAD returns the object attribute $io_object->{format_template_} if that key exists.

AUTOLOAD returns $io_object->_format_template if that method exists.

AUTOLOAD returns $track->format_template if $track has that method.

Otherwise AUTOLOAD throws an exception.

If the object has defined a method "format_template" (no underscore) the call is resolved immediately. AUTOLOAD is never invoked, giving the method definition the highest priority.

Discussion

While this was an achievement to figure out and to debug, I arrived at a clear design that meets my requirements.

I cannot say whether my solution is sufficiently general, sufficiently robust, or represents appropriate use of an appropriate hammer.

That is one reason I am writing this! I would like to know if I'm missing a better solution in the OO framework world.

Code

Here is code for the base class (with a few cosmetic changes)

package IO;

our $AUTOLOAD;


# The template code below gets a comment-stripped list
# of attributes from the source file "io_fields" and appends
# an underscore to each key

use Object qw([% join " ",map{$_."_" }split " ", qx(./strip_all ./io_fields) %]);

# IO objects for writing Ecasound chain setup file
#
# Object attributes can come from three sources:
#
# 1. As arguments to the constructor new() while walking the
#    routing graph:
#      + assigned by dispatch: chain_id, loop_id, track, etc.
#      + override by graph node (higher priority)
#      + override by graph edge (highest priority)
# 2. methods called as $object->method_name
#      + defined as _method_name (access via AUTOLOAD, overrideable by constructor)
#      + defined as method_name  (not overrideable)
# 3. AUTOLOAD
#      + any other method calls are passed to the the associated track
#      + illegal track method call generates an exception

sub new {
    my $class = shift;
    my %vals = @_;
    my @args = map{$_."_", $vals{$_}} keys %vals; # add underscore to key

    # note that we won't check for illegal fields
    # to allow for AUTOLOAD resolution

    bless {@args}, $class
}
sub AUTOLOAD {
    my $self = shift;
    # get last part of method call
    my ($call) = $AUTOLOAD =~ /([^:]+)$/;
    my $field = "$call\_";
    my $method = "_$call";
    return $self->{$field} if exists $self->{$field};
    return $self->$method if $self->can($method);
    if ( my $track = $Track::by_name{$self->{track_}} ){
        return $track->$call if $track->can($call)
        # ->can is reliable here because Track has no AUTOLOAD
    }
    print $self->dump;
    croak "Autoload fell through. Object type: ", (ref $self), ", illegal method call: $call\n";
}

7 Comments

I think we have an XY problem here. As far as I could follow, you presented your solution to a design problem, and you’re asking if Moose will let you do something similar.

But you never explained the original design problem for which you came up with this solution.

It is well possible and even likely that the solution with Moose will be entirely different, but there is no way to know what, because the problem it has to solve is not adequately known.

So… what does all this infrastructure do for you? What is it that it makes easy, and why is it useful that that thing is easy to do?

"I think we have an XY problem here."

... and this is where you should stop paying any attention. "XY problem" is a condescending way to say "I don't want to answer your question" and/or another way of saying "you don't know what you're trying to do, but I do."

If you say so. The rest of my comment clearly refutes both of your chosen interpretations, but apparently you didn’t even stick with it long enough to see that.

Joel got warnocked… what is the reason for that, did you wonder? Because I think I know it.

However… he’ll probably find the answers on his own in due time anyway. So in the end I suppose I must agree with you: I should simply have let the silence happen, like everyone else did. Mea culpa.

Yes I will. :-)

I read both “everything” feeds from blogs.perl.orgall entries and all comments – so I see everything that gets posted on the site, no matter when and where.

Leave a comment

About Joel Roth

user-pic I blog about Perl.