Monday 20 October 2014

Method Missing And AUTOLOAD

I firstly encountered the ability for an object to invoke a default method when an invokation is attempted on a missing one many years ago, when all the frenzy (at the time) around Ruby prompted me to have a look into the language. This feature in Ruby is called method_missing, and at the moment seemed to me like the most "innovative" feature that I could find in the language (for different reasons I didn't like the language too much and have never paid it any attention ever since). At that time, when JavaScript seemed stagnated because of the very different views (wars) on how the language should evolve (all that ECMAScript 4 madness), Mozilla decided to push the language forward adding different experimental features, some of them (generators, let scope, destructured assignment) will finally make it into ES6, others have fallen into oblivion, some of them sadly (I loved the list comprehensions thing copied from Python). One of them was __noSuchMethod__, that as the name suggests, was the same as method_missing.

Years later I came across this same feature in Groovy, and at in parallel the same feature was made possible in C# through TryInvokeMember when using dynamic. All this made me think of this as a state of the art feature (but to my surprise I've found a long list here of languages supporting this idea), so when I started to learn Perl I felt shocked when finding out that such an apparently deprecated language (the true is that the language continues to grow in power and is at this moment a really, really modern language (though quirky, that's for sure)) could sport such a modern feature. The support for this is given under the name of AUTOLOAD, and though I guess it was not born with O.O. programming, proxies or AOP in mind... it really plays nicely.

The basic use is like this:

package Person;
	use strict; use warnings;

    sub New {
		my $class = shift;
		my $self = {
			Name => shift
		};
		bless $self, $class;
		return $self;
	}

    sub SayHi{
		my $self = shift;
		return $self->{Name} . " is saying Hi";
	}
	sub AUTOLOAD {
        my $self = shift;
		my $methodMissing = our $AUTOLOAD;
		$methodMissing =~ s/.*:://;
		
		#do not intercept calls to a non defined DESTROY 
		if($methodMissing eq 'DESTROY') {return;}

        print "unknown method: " . $methodMissing . " invoked on " . $self->{Name} . " with params : " . join(', ', @_);
    }
1;

Notice that I've added a line to prevent action when a missing DESTROY is invoked.

And thanks to it creating Proxies is so damn simple like this:

package AroundProxy;
	use strict; use warnings;

    sub New {
		my ($class, $targetObj, $interceptorFunc) = @_;
		my $self = {
			targetObj => $targetObj,
			interceptorFunc => $interceptorFunc
		};
		bless $self, $class;
		return $self;
	}

	sub AUTOLOAD {
        my $self = shift;
		my $methodName = our $AUTOLOAD;
		$methodName =~ s/.*:://;
		
		#do not intercept calls to DESTROY (as most classes won't define it and we'll get a warning)
		if($methodName eq 'DESTROY') {return;}
		
		return $self->{interceptorFunc}->($methodName, $self->{targetObj}, @_);
    }
1;

sub aroundTest{
	print "". (caller(0))[3] . "\n";
	my ($p1) = @_;
	my $proxy = AroundProxy->New($p1, 
		sub{
			my $methodName = shift;
			my $targetObj = shift;
			print "$methodName started with params:" . join(", ", @_) . "\n";
			my $res = $targetObj->$methodName(@_);
			print "$methodName finished\n";
			return $res;
		}
	);
	print $proxy->SayHi() . "\n";
}

Bear in mind that Perl supports multiple inheritance. If you had a class inheriting from several classes that in turn define AUTOLOAD, the AUTOLOAD that will be invoked will be the one of the leftmost class in the inheritance chain (@ISA array), just the same that is done to resolve any other method.

I won't close this post without taking the opportunity to complain about ES6 not including this feature. Some will argue that it's covered by ES6 proxies (using the get trap), but it's quite different. With proxies you're creating a new object to wrap the existing one, if we had methodMissing we would have it directly in the object, which means the possibility of addressing more complex use cases: we could expand an existing object with methodMissing and later on remove it, so we could be enable/disable dynamically the feature in one object

No comments:

Post a Comment