|
Nothing requires objects to be implemented as hash references. An object can be any sort of
reference so long as its referent has been suitably blessed. That means scalar, array, and code
references are also fair game.
A scalar would work if the object has only one datum to hold. An array would work for most
cases, but makes inheritance a bit dodgy because you have to invent new indices for the derived
classes.
If the user of your class honors the contract and sticks to the advertised interface, then
you can change its underlying interface if you feel like it. Here's another implementation that
conforms to the same interface specification. This time we'll use an array reference instead of
a hash reference to represent the object.
package Person;
use strict;
my($NAME, $AGE, $PEERS) = ( 0 .. 2 );
############################################
## the object constructor (array version) ##
############################################
sub new {
my $self = [];
$self->[$NAME] = undef; # this is unnecessary
$self->[$AGE] = undef; # as is this
$self->[$PEERS] = []; # but this isn't, really
bless($self);
return $self;
}
sub name {
my $self = shift;
if (@_) { $self->[$NAME] = shift }
return $self->[$NAME];
}
sub age {
my $self = shift;
if (@_) { $self->[$AGE] = shift }
return $self->[$AGE];
}
sub peers {
my $self = shift;
if (@_) { @{ $self->[$PEERS] } = @_ }
return @{ $self->[$PEERS] };
}
1; # so the require or use succeeds
|
|
You might guess that the array access would be a lot faster than the hash access, but they're
actually comparable. The array is a little bit faster, but not more than ten or fifteen
percent, even when you replace the variables above like $AGE with literal numbers, like 1. A
bigger difference between the two approaches can be found in memory use. A hash representation
takes up more memory than an array representation because you have to allocate memory for the
keys as well as for the values. However, it really isn't that bad, especially since as of
version 5.004, memory is only allocated once for a given hash key, no matter how many hashes
have that key. It's expected that sometime in the future, even these differences will fade into
obscurity as more efficient underlying representations are devised.
Still, the tiny edge in speed (and somewhat larger one in memory) is enough to make some
programmers choose an array representation for simple classes. There's still a little problem
with scalability, though, because later in life when you feel like creating subclasses, you'll
find that hashes just work out better.
Using a code reference to represent an object offers some fascinating possibilities. We can
create a new anonymous function (closure) who alone in all the world can see the object's data.
This is because we put the data into an anonymous hash that's lexically visible only to the
closure we create, bless, and return as the object. This object's methods turn around and call
the closure as a regular subroutine call, passing it the field we want to affect. (Yes, the
double-function call is slow, but if you wanted fast, you wouldn't be using objects at all, eh?
:-)
Use would be similar to before:
use Person;
$him = Person->new();
$him->name("Jason");
$him->age(23);
$him->peers( [ "Norbert", "Rhys", "Phineas" ] );
printf "%s is %d years old.\n", $him->name, $him->age;
print "His peers are: ", join(", ", @{$him->peers}), "\n";
|
|
but the implementation would be radically, perhaps even sublimely different:
package Person;
sub new {
my $that = shift;
my $class = ref($that) || $that;
my $self = {
NAME => undef,
AGE => undef,
PEERS => [],
};
my $closure = sub {
my $field = shift;
if (@_) { $self->{$field} = shift }
return $self->{$field};
};
bless($closure, $class);
return $closure;
}
sub name { &{ $_[0] }("NAME", @_[ 1 .. $#_ ] ) }
sub age { &{ $_[0] }("AGE", @_[ 1 .. $#_ ] ) }
sub peers { &{ $_[0] }("PEERS", @_[ 1 .. $#_ ] ) }
1;
|
|
Because this object is hidden behind a code reference, it's probably a bit mysterious to
those whose background is more firmly rooted in standard procedural or object-based programming
languages than in functional programming languages whence closures derive. The object created
and returned by the new() method is itself not a data reference as we've seen before. It's an
anonymous code reference that has within it access to a specific version (lexical binding and
instantiation) of the object's data, which are stored in the private variable $self. Although
this is the same function each time, it contains a different version of $self.
When a method like $him->name("Jason") is called, its implicit
zeroth argument is the invoking object--just as it is with all method calls. But in this case,
it's our code reference (something like a function pointer in C++, but with deep binding of
lexical variables). There's not a lot to be done with a code reference beyond calling it, so
that's just what we do when we say &{$_[0]}. This is just a regular function
call, not a method call. The initial argument is the string "NAME", and any remaining
arguments are whatever had been passed to the method itself.
Once we're executing inside the closure that had been created in new(), the $self hash
reference suddenly becomes visible. The closure grabs its first argument ("NAME" in
this case because that's what the name() method passed it), and uses that string to subscript
into the private hash hidden in its unique version of $self.
Nothing under the sun will allow anyone outside the executing method to be able to get at
this hidden data. Well, nearly nothing. You could single step through the program using
the debugger and find out the pieces while you're in the method, but everyone else is out of
luck.
There, if that doesn't excite the Scheme folks, then I just don't know what will. Translation
of this technique into C++, Java, or any other braindead-static language is left as a futile
exercise for aficionados of those camps.
You could even add a bit of nosiness via the caller() function and make the closure refuse to
operate unless called via its own package. This would no doubt satisfy certain fastidious
concerns of programming police and related puritans.
If you were wondering when Hubris, the third principle virtue of a programmer, would come
into play, here you have it. (More seriously, Hubris is just the pride in craftsmanship that
comes from having written a sound bit of well-designed code.)
|
|