Almost every modern programming language has an object-oriented extension or variant available. Some languages, such as SmallTalk and Ruby are “pure” OO languages in that everything in the language is an object29. Other languages such as Perl and this PScheme implementation add OO features to what is essentially a procedural core.
Every object implementation has its peculiarities. There are a lot of trade-offs and choices to be made. Most of these differences come down to issues of visibility of object components from other parts of a program: should the fields of an object be visible at all outside of that object? Should an object be able to see the fields in an object it inherits from? Should an object of a particular class be able to see fields of another object of the same class? Should certain methods of an object be hidden from the outside world? from its descendants?
The implementation discussed here makes choices in order to leverage existing code. Those choices result in a particular OO “style”. I've also decided, somewhat perversely, to be as different from the Perl 5 object implementation as possible within the constraints imposed, in order to give the reader a sense of the different choices that are available.
Rather than listing the features up front, let's start off with some examples of the extension in action, showing its syntax etc. We can pick up the semantics as we go along.
First of all a new special form called make-class
creates a new class. It returns that new class as its value. The
syntax of make-class
is:
(make-class ‹parent-expression› (‹field› ...) ‹method› ...)
‹parent-expression› is an expression evaluating to another class. Each ‹field› is a symbol naming one of the object's fields. Each ‹method› has the form:
(‹name› (‹arg› ...) ‹body›)
where ‹name› is the name of the method, the
‹arg›s are the arguments, and ‹body› is the
body, much like lambda
expressions. Also, somewhat like lambda
expressions, but not identically, method bodies capture the lexical environment current
when the class is created.
The system provides a pre-built root class to act as a starting point for any class hierachy. That class is bound to the symbol root.
So here is how we might create a crude “bank account” class:
(define Account (make-class root (balance) (init (amount) (set! balance amount)) (deposit (amount) (set! balance (+ balance amount))) (withdraw (amount) (set! balance (- balance amount))) (balance () balance) (clone () (class balance))))
make-class
returns the new class, and we bind that
to the symbol Account
with define
.
Our new Account
descends directly from the root
class. It has a single balance
field, and five methods
called init
, deposit
, withdraw
,
balance
and clone
. Note that there is no conflict between the
field called balance
and the method of the same name:
methods exist in a separate namespace.
The init method is special. It will get called whenever a new object is created. It should normally assign values to the object's fields, since they initially all have a value of zero.
We'll come back to that clone
method in a bit.
Creating instances of Account
simply involves invoking
the Account
class with whatever arguments its init
method takes. It will return an object of class Account
,
suitably initialised:
(define my-account (Account 20))
This creates an object of class Account
with an initial
balance
of 20
, since init
assigns
the argument 20
to the balance
field.
define
binds the new object to the symbol my-account
.
This starts to explain that mysterious clone
method. All methods
have access to a special variable called class
, that refers
to the class of the object;
this has some parallels with the Perl __PACKAGE__
identifier.
So clone
need only call
(class balance)
to create a copy of the current object.
To call a method on an object you invoke the object, with the method name as the first argument and arguments to the method itself as the remaining arguments:
(my-account deposit 10)
The deposit
method takes the argument 10
and adds it to the existing balance
.
Classes and objects wouldn't be much fun without inheritance, so here's an example of a derived class:
(define InterestAccount (make-class Account (rate) (init (interest amount) (begin (super init amount) (set! rate interest))) (accumulate () (this deposit (* (this balance) rate)))))
Note a few things in particular.
Account
, the class
we created previously.InterestAccount
class adds an extra field,
rate
.InterestAccount
's init
method, before
setting the new object's rate
to interest
, invokes
the parent's init
method with the call (super init
amount)
to set the balance
. This is more or less equivalent to the
Perl SUPER
method qualifier:
$self->SUPER::init($amount);The super object is an implicit field of the class, and is automatically initialised when an object is created. It represents the parent object.
$self
conventionally does for
perl methods30.InterestAccount
class cannot see its parent's
fields, only its methods. It has to call (this balance)
to get the value of balance
and (this deposit
‹arg›)
to change it.this
or super
to call a method on the
current object.The InterestAccount
class can be used as follows:
> (define my-account (InterestAccount 20 2)) my-account > (my-account deposit 10) 30 > (my-account balance) 30 > (my-account accumulate) 90 > (my-account balance) 90
Quite a nice rate of interest that is.
Although there is no direct support for class variables and
methods, the fact that classes, and hence methods, capture the
current environment allows us to fake them. Here's a variant
on the Account
class that keeps track of the total
amount of money in all account objects:
(define Account (let ((total 0)) (make-class root (balance) (set-balance (op amount) (begin (set! balance (op balance amount)) (set! total (op total amount)))) (init (amount) (this set-balance + amount)) (deposit (amount) (this set-balance + amount)) (withdraw (amount) (this set-balance - amount)) (balance () balance) (total () total))))
The let
binds total
to an initial value
of 0
then evaluates the make-class
construct
in this new environment. The newly created class captures that
environment. That new class is then returned by the let
expression and bound to Account
.
Every instance of Account
will share the lexically
scoped variable total
. Rather than change each of
init
, deposit
and withdraw
to
individually maintain both the value of total
and
balance
, a new method set-balance
has been
added. It takes an operation op
(+
or
-
) and an amount
and applies the operation
with the amount
separately to both the balance
and the total
.
The init
, deposit
and withdraw
methods have been modified to use this new method, and another new
method, total
, provides read-only access to the value
of total
.
super
CallsWe're not quite finished with the details.
There's one last wrinkle to deal with reguarding that super
object mentioned earlier. Although on the surface it may appear to behave just like
any other object, a call to a method on the super
object
cannot just be a simple method invocation. If it was, then
this
in the called method would be the object referred
to by super
in the calling method. Instead the this
in the called method should continue to refer to the this
that was current before the super
call. To make that
clear, consider a rather contrived example:
> (define cat > (make-class > root > () > (poke () (this respond)) > (respond () 'purr) > )) cat > (define lion > (make-class > cat > () > (poke () (super poke)) > (respond () 'roar) > )) lion > (define leo (lion)) leo > (leo poke) roar
leo
is an instance of lion
and the call
(leo poke)
invokes lion
's poke
method.lion
's poke
method does (super poke)
,
which invokes cat
's poke
method.cat
's poke
method does (this respond)
but since, even after the super
call, this
is still the originating object leo
of class lion
,
it is lion
's respond
method that gets invoked, resulting
in “roar
” rather than the “purr
” from cat
's
respond
method.Here's a summary, then, of the main features of our implementation
this
is always available as an
implicit argument to each method and refers to the object that the
method was originally invoked on.super
is always available
as an implicit field of every object
and refers to the parent object of the object that owns the method.super
object passes
this
, not super
, as the implicit object argument
to the called method.class
is always available as an
implicit field in every object and refers to the class of the object.So how do we go about implementing this extension? Well, to be
frank, in a fairly ad hoc way. It should be obvious that PScheme
objects have a lot in common with environments, namely that they
store the values of variables. Our existing environment implementation
can easilly be pressed in to service as a PScheme object. In fact
in this implementation objects are environments, or rather,
chains of environments linked via a super
field where
each individual environment represents an instance of the equivalent
class in the class hierachy.
We need only add a couple of methods to our existing environment implementation to get all we need.
Apply()
,
since now environments are exposed in the language as objects and are invoked
as operators:
(‹object› ‹method› ‹arg›...)
.lookup_method()
method because we've said that PScheme methods live in a separate
namespace from normal fields. We can always recognise PScheme method
invocation and hence the PScheme method name by context: it is
always the first “argument” to an object. We cheat egregiously
here and just use the class
binding in each PScheme
object to locate the PScheme class, and check to see if the PScheme
method is in it. If not found then lookup_method()
recurses
down the chain of objects via the super
binding and tries
again. Of course we need an object to represent PScheme classes, but
that is just going to contain the parent PScheme class, the fields and
methods of the class, and the environment that was current at the
point of its creation.
That nascent PScheme Class package will need an Apply()
method
that will create PScheme objects on demand, passing any arguments
to that object's nearest init
method.
That is pretty much all we need to do. Methods can act just like
closures but will extend the environment representing the object
when they are called. There are a few fiddly details around method
invocation on a super
object, but we'll deal with that
later.
make-class
Starting from the top, the special form
(make-class ‹parent-expr› (‹field›...)
‹method›...)
is simple to implement, it just returns an instance of that yet to
be described PScm::Class object. Hopefully it's not too
counterintuitive that PScheme classes are in fact Perl objects.
Here's the implementation of make-class
in a new
PScm::SpecialForm::MakeClass package:
178 package PScm::SpecialForm::MakeClass; 179 180 use base qw(PScm::SpecialForm); 181 182 sub Apply { 183 my ($self, $form, $env) = @_; 184 185 my $parent_expr = $form->first; 186 my $fields = $form->rest->first; 187 my $methods = $form->rest->rest; 188 my $parent_class = $parent_expr->Eval($env); 189 return PScm::Class->new($parent_class, 190 $fields, 191 $methods, 192 $env); 193 } 194 195 1;
As is usual for a special form it just has an Apply()
method.
On Line 185
that unpacks the parent expression, fields and methods from the
argument $form
, and on Line 188 it evaluates
the parent expression in the current environment to get an actual
parent class (PScm::Class) object. Finally on Line 189 it returns a new
instance of PScm::Class capturing those values and the
current environment.
The new()
method in PScm::Class doesn't do
anything too clever either, On
Line 10
it declares a hashref $rh_methods
, and then on
Line 11
it calls a helper static method _populate_methods_hash()
which will
chop up the PScheme methods into names, arguments
and bodies, storing each pair of args and body in the hash keyed on
the PScheme method name. Then starting on Line 13 it returns a new instance containing that hash along
with the parent PScheme class, fields and current environment:
7 sub new { 8 my ($class, $parent, $fields, $methods, $env) = @_; 9 10 my $rh_methods = {}; 11 $class->_populate_methods_hash($rh_methods, $methods); 12 13 return bless { 14 parent => $parent, 15 fields => [$fields->value], 16 methods => $rh_methods, 17 env => $env, 18 }, $class; 19 }
Here's _populate_methods_hash()
:
21 sub _populate_methods_hash { 22 my ($class, $rh_methods, $methods) = @_; 23 if ($methods->is_pair) { 24 my $method = $methods->first; 25 my ($name, $args, $body) = $method->value; 26 $rh_methods->{ $name->value } = 27 { args => $args, body => $body }; 28 $class->_populate_methods_hash($rh_methods, $methods->rest); 29 } 30 }
That's it for make-class
. Following our previous
course, we next need to look at PScheme object creation, which
occurs when a PScheme class is invoked with arguments intended for
its init
method:
(‹class› ‹arg›...)
.
It's probably worth stepping back at this stage to clarify how
our environments will be arranged into objects. As I've already
mentioned each PScheme object (environment) extends the environment that
was captured by its class. Furthermore each PScheme object (environment)
has a super
field referring to the anonymous PScheme object
created by its parent PScheme class.
That means we end up with a situation something like Figure 12.1
Although this figure does not tell the whole story, it at least
emphasizes that a PScheme object consists of a number of environment frames,
one for each class in the equivalent PScheme class hierachy, but those environment
frames are connected not by a direct parent/child relationship but via
an ordinary variable in each frame called super
. The environment
frames representing each object extend the environment that their respective
classes captured. This is implied, but not shown, by the unterminated arrows
in the figure.
So we were about to take a look at how PScheme classes create PScheme objects.
Classes create objects when they are invoked
as (‹class› ‹arg›...)
.
To make anything invokeable we just need give it an
Apply()
method, and here's the one for PScm::Class:
32 sub Apply { 33 my ($self, $form, $env) = @_; 34 35 my $new_object = $self->make_instance(); 36 $new_object->call_method($new_object, "init", $form, $env); 37 return $new_object; 38 }
On Line 35 it calls
a make_instance()
method to create a
new PScheme object (really a PScm::Env).
Then on
Line 36
it calls the PScm init
method of the new object.
This is done using a call_method()
method of
PScm::Env. This takes
the PScheme object on which the method is being invoked ($new_object
,
which will be passed to the PScheme method as this
),
the name of the PScheme method ("init"
), and
the arguments to the method itself ($form
and $env
.)
We'll look at call_method()
later.
The make_instance()
method of PScm::Class
must recurse
down to the root of the PScheme class hierachy, creating a chain of
anonymous PScheme objects on the way back up, each linked to its parent
by a super
field. Here it is:
40 sub make_instance { 41 my ($self) = @_; 42 43 my $parent_instance = $self->{parent}->make_instance(); 44 45 return $self->{env}->ExtendUnevaluated( 46 new PScm::Expr::List( 47 PScm::Expr::Symbol->new("class"), # $self 48 PScm::Expr::Symbol->new("super"), # $parent_instance 49 @{ $self->{fields} }, # 0... 50 ), 51 new PScm::Expr::List( 52 $self, # "class" 53 PScm::Env::Super->new(super => $parent_instance), # "super" 54 ((PScm::Expr::Number->new(0)) x @{ $self->{fields} }), # field... 55 ) 56 ); 57 }
The first thing it does, on
Line 43
is to call its parent PScheme class' make_instance()
method to
get an instance of its parent class. Then starting on
Line 45
the rest of the method extends the
environment that the PScm::Class object captured when it was created, with appropriate
bindings from class
to $self
, super
to the $parent_instance
and from each of the PScheme classes fields
to initial values of zero. It is this new environment that is returned by
make_instance()
.
If you were reading the above code carefully,
you'd have noticed that the super
field does not link
directly to the parent instance, but via a derivative of PScm::Env
called PScm::Env::Super. This is so that the super
object can have a separate Apply()
method. That gives the lie to our simple
picture of environments-as-objects in Figure 12.1.
In fact the true situation is shown in Figure 12.2.
To keep things simple this figure only shows an object whose
immediate parent is root
.
You can see that the PScheme object is joined to its parent via a
PScm::Env::Super object bound to its super
field, and that the PScm::Env::Super object also has a
super
field providing the link to the real parent.
Additionally each PScheme object has a class
binding referring to the PScheme class that created it. That is
a PScm::Class for all but the root object, which has
no super
binding and has a class
binding that refers to a PScm::Class::Root object.
PScm::Class::Root is a derivative of PScm::Class,
and it is a PScm::Class::Root instance that will be bound to
root
in the initial environment.
That conveniently brings us back round to the make_instance()
method, and how that recursive call to the parent PScheme class'
make_instance()
is terminated.
That happens when it hits the make_instance()
method of
the PScm::Class::Root package, shown next.
70 package PScm::Class::Root; 71 72 use base qw(PScm::Class); 73 74 sub new { 75 my ($class, $env) = @_; 76 77 return bless { 78 parent => 0, 79 fields => [], 80 methods => {}, 81 env => $env, 82 }, $class; 83 } 84 85 sub make_instance { 86 my ($self) = @_; 87 88 return $self->{env} 89 ->ExtendUnevaluated( 90 new PScm::Expr::Symbol("class"), 91 $self 92 ); 93 } 94 95 1;
The new()
method on
Lines 74–83
is just meant to be easy to call from the repl where the root
class will be initialised. It creates a PScheme class with no parent,
no fields, no methods, and whatever env
is passed
in31.
The make_instance()
method on
Lines 85–93
is not recursive, it just extends the captured environment
with a binding of class
to $self
(the PScm::Class::Root object,) returning the result.
Note that it takes advantage of the fact that ExtendUnevaluated()
can cope with a single symbol and value as well as lists of the same.
init
Method InvocationSo far we've looked at how PScheme classes are created and how they in turn
create PScheme objects. Next we're going to look at how PScheme methods are invoked,
starting with the init
method.
If you remember, the PScheme init
method is called
by PScm::Class::Apply()
when creating a new object,
by calling call_method()
on the instance $new_object
returned by
PScm::Class::make_instance()
.
Since $new_object
is a PScm::Env,
call_method()
must be a method of PScm::Env,
and here it is.
168 sub call_method { 169 my ($self, $this, $method_name, $args, $env) = @_; 170 171 if (my $method = $self->_lookup_method($method_name)) { 172 return $method->ApplyMethod($this, $args, $env); 173 } 174 }
call_method()
is passed both the “real” perl object
$self
and the object representing PScheme's idea of
the current object, $this
, that the PScheme method is being
invoked on. Normally these are one and
the same. Additionally it is passed the method name, arguments
and another environment in which the arguments are to be evaluated
if a method is found.
On
Line 171
it uses _lookup_method()
(discussed next) to find the method, and if found
then on
Line 172
it invokes the PScheme method by calling its ApplyMethod()
and returns the result. If no method can be found it returns undef
,
and since in the case of PScm::Class' Apply()
the result of
calling init
is discarded anyway, it is not fatal if
an init
method is not found.
_lookup_method()
employs a simple strategy to locate a method.
First it checks in the current PScheme object's class
, and if it
can't find the method there, it recurses to its super
object.
That leads to the equally simple definition below.
136 sub _lookup_method { 137 my ($self, $method_name) = @_; 138 139 return $self->_lookup_method_here($method_name) 140 || $self->_lookup_method_in_super($method_name); 141 }
So _lookup_method()
breaks down into two simpler methods:
_lookup_method_here()
and _lookup_method_in_super()
,
which it tries in turn. _lookup_method_here()
is similarily
simple.
143 sub _lookup_method_here { 144 my ($self, $method_name) = @_; 145 146 if (exists $self->{bindings}{class}) { 147 return $self->{bindings}{class} 148 ->get_method($method_name, $self); 149 } 150 }
It checks to see if the current object has a class
binding, and if so it calls get_method()
on the class,
returning the result.
get_method()
will return undef
if it can't
find the method in the class, and _lookup_method_here()
returns undef
if there is no class
binding.
_lookup_method_in_super()
is equally simple.
152 sub _lookup_method_in_super { 153 my ($self, $method_name) = @_; 154 155 if (exists $self->{bindings}{super}) { 156 return $self->{bindings}{super} 157 ->_lookup_method($method_name); 158 } 159 }
It checks to see if the current PScheme object has a super
,
and if so it calls _lookup_method()
on it. Otherwise it
returns undef
.
Since _lookup_method()
, _lookup_method_here()
and _lookup_method_in_super()
are all methods of
PScm::Env, they are all available to PScm::Env::Super
where they work without modification: super
objects
have a super
field but no class
field.
Going back to _lookup_method_here()
, if that found a
class
binding, it called get_method()
on the
PScheme class, passing it the method name to look for, and perhaps
less obviously, $self
as well. Here's what get_method()
back in PScm::Class does with those arguments.
59 sub get_method { 60 my ($self, $method_name, $object) = @_; 61 62 if (exists $self->{methods}{$method_name}) { 63 return PScm::Closure::Method->new( 64 $self->{methods}{$method_name}{args}, 65 $self->{methods}{$method_name}{body}, $object); 66 } 67 }
On
Line 62
it looks in its methods
subhash
for a key matching the string $method_name
.
If it finds one it knows it has found the method
and returns a new instance of
PScm::Closure::Method, a closure just like a lambda expression,
containing the relevant method args and method body from the subhash, and most importantly
capturing the environment $object
. Reasoning backwards, this is correct,
$object
(the $self
from _lookup_method_here()
) is the environment in which the method was found, (via class
)
and that is the
environment that the method should extend when it executes, so that the method
body can “see” the fields of the object.
I'd just like to emphasize a point here, the object $object
passed to get_method()
is not necessarily the same
as the this
that will be passed to the method when it
executes. That would only be true if the method was found in the
first PScheme object that _lookup_method()
looked in.
There's very little left to cover now. We just need to take a look at PScm::Closure::Method. This is a subclass of PScm::Closure, as you can see.
67 package PScm::Closure::Method; 68 69 use base qw(PScm::Closure); 70 71 sub new { 72 my ($class, $args, $body, $env) = @_; 73 74 bless { 75 args => PScm::Expr::List->Cons( 76 PScm::Expr::Symbol->new("this"), $args 77 ), 78 body => $body, 79 env => $env, 80 }, $class; 81 } 82 83 sub ApplyMethod { 84 my ($self, $this, $form, $env) = @_; 85 my $evaluated_args = $form->map_eval($env); 86 return $self->_apply(PScm::Expr::List->Cons($this, $evaluated_args)); 87 } 88 89 1;
The new()
method on
Lines 71–81
is the one we just saw being called by get_method()
.
What differentiates it from the normal PScm::Closure::Function
new()
method is that on
Line 75
it prepends the symbol this
to the argument list as
it constructs the closure. That “implicit” argument will be supplied
by ApplyMethod()
which you can also see in this package.
The defining feature of a closure is that it captures an environment when it is created and extends it when it is executed. These method closures are no different, but the environment that they capture is the object in whose class the method was found. Hence method bodies can see the fields of the object as normal variables: they are normal variables.
ApplyMethod()
also behaves pretty much like the normal
closure's Apply()
, but it differs in having an extra $this
argument.
On
Line 85
it calls map_eval()
on the argument $form
with
the current environment to get a list of evaluated arguments, just
as the normal closure's Apply()
does. But then
on
Line 86
it prepends $this
(the PScheme this
) to those
actual arguments when calling the generic PScm::Closure
_apply()
method. This ties in with the new()
method having supplied an extra symbol this
to the
list of formal arguments.
We've now covered everything to do with PScheme object creation and initialisation
in PScheme. Along the way we've seen, by following the process of calling
an object's init
method, most of the machinery behind method
invocation. There are only two remaining details to fill in.
The first of those is normal PScheme method invocation. That is done by invoking the object with the method name and arguments, for example:
(my-account deposit 10)
Since objects (environments) are now directly invokeable, they too must
have an Apply()
method, shown here:
176 sub Apply { 177 my ($self, $form, $env) = @_; 178 179 my ($method_symbol, $args) = ($form->first, $form->rest); 180 my $res = 181 $self->call_method($self, $method_symbol->value, $args, $env); 182 183 if (defined $res) { 184 return $res; 185 } else { 186 die "method ", $method_symbol->value, " not found\n"; 187 } 188 }
On
Line 179
it splits the argument $form
into a method name (a symbol) and a list
of arguments to the method. Then on
Line 181
it attempts to call the method, and collects the result. Now the result will
only be undefined if a method could not be found, in which case an exception
is raised. Otherwise the result is returned.
super
Method InvocationThe only remaining piece is invocation of a PScheme method on a super
object. super
is bound to a PScm::Env::Super instance,
which in turn has a super
binding referring to the next object
in the chain. Here's PScm::Env::Super:
191 package PScm::Env::Super; 192 193 use base qw(PScm::Env); 194 195 sub Apply { 196 my ($self, $form, $env) = @_; 197 198 my ($method, $args) = ($form->first, $form->rest); 199 my $this = $env->LookUp(PScm::Expr::Symbol->new("this")); 200 my $res = 201 $self->call_method($this, $method->value, $args, $env); 202 203 if (defined $res) { 204 return $res; 205 } else { 206 die "method ", $method->value, " not found in super\n"; 207 } 208 } 209 210 1;
If you compare the Apply()
method here with the one in PScm::Env
above, you can see they differ in that on
Line 199
the Apply()
looks up this
in the current environment. Then on
Line 201
it passes $this
instead of $self
to call_method()
.
The upshot of that is the variable $this
will be the one that
gets bound to the implicit this
argument to the PScheme method when it is
invoked.
And finally, we just need to see how the new object code is wired into the repl.
Here's ReadEvalPrint()
for version 0.0.9 of our interpreter.
32 sub ReadEvalPrint { 33 my ($infh, $outfh) = @_; 34 35 $outfh ||= new FileHandle(">-"); 36 my $reader = new PScm::Read($infh); 37 my $initial_env = new PScm::Env( 38 let => new PScm::SpecialForm::Let(), 39 '*' => new PScm::Primitive::Multiply(), 40 '-' => new PScm::Primitive::Subtract(), 41 '+' => new PScm::Primitive::Add(), 42 if => new PScm::SpecialForm::If(), 43 lambda => new PScm::SpecialForm::Lambda(), 44 list => new PScm::Primitive::List(), 45 car => new PScm::Primitive::Car(), 46 cdr => new PScm::Primitive::Cdr(), 47 cons => new PScm::Primitive::Cons(), 48 letrec => new PScm::SpecialForm::LetRec(), 49 'let*' => new PScm::SpecialForm::LetStar(), 50 eval => new PScm::SpecialForm::Eval(), 51 macro => new PScm::SpecialForm::Macro(), 52 quote => new PScm::SpecialForm::Quote(), 53 'set!' => new PScm::SpecialForm::Set(), 54 begin => new PScm::SpecialForm::Begin(), 55 define => new PScm::SpecialForm::Define(), 56 'make-class' => new PScm::SpecialForm::MakeClass(), 57 ); 58 59 $initial_env->Define( 60 PScm::Expr::Symbol->new("root"), 61 PScm::Class::Root->new($initial_env) 62 ); 63 64 while (defined(my $expr = $reader->Read)) { 65 my $result = $expr->Eval($initial_env); 66 $result->Print($outfh); 67 } 68 }
The changes are in bold. On
Line 41
I finally caved in and added primitive addition as a builtin.
I leave it to the reader to do the same.
On
Line 56
you can see the additional binding of make-class
to a PScm::SpecialForm::MakeClass object, and on
Line 59
we attach a new PScm::Class::Root to the symbol
root
in the initial environment. That needs to be done
using Define()
because we need to pass the value of
$initial_env
to the new()
method of
PScm::Class::Root
This object extension added new methods to PScm::Env
to allow environments to behave as operators within the language.
It added new classes
PScm::SpecialForm::MakeClass to implement the
make-class
special form, PScm::Class and
PScm::Class::Root to host our PScheme class code,
PScm::Env::Super to provide an alternative Apply()
method for super
method invocation, and
PScm::Closure::Method for the modified closure behaviour
of PScheme methods. To summarise:
make-class
in the initial environment creates instances of
PScm::Class
when called with arguments parent
, fields
and methods
.root
in the initial environment provides a
base class in which other classes can be rooted.Apply()
method, and when a
PScm::Class is invoked with arguments, that Apply()
method first creates
a PScm object, which is in fact just an instance of a PScm::Env,
then calls that new object's init
method with the arguments
that were passed to the class.Apply()
calls make_instance()
which recurses down the chain of PScheme classes
to the root
and creates a chain of objects (environments)
on the way back up
class
binding
referring to the PScm::Class instance that created it.super
binding
referring to a PScm::Env::Super object
which itself has a super
binding referring to the actual parent object.init
or otherwise, the
call_method()
method of PScm::Env
is used. This uses _lookup_method()
to locate the method and create an instance of
PScm::Closure::Method from it. If a method is found
_call_method()
invokes the method's ApplyMethod()
._lookup_method()
looks first in the current
environment for a class
binding and if found
checks the class for the method, otherwise it recurses
on the super
field.Apply()
in PScm::Env passes $self
(the object on which the method is being invoked)
as the value of this
to call_method()
.Apply()
method of PScm::Env::Super
instead looks up the value of this
in the current
environment and passes that as the value of this
to call_method
.get_method()
creates an instance of
PScm::Closure::Method, a closure which captures the
environment (object) in whose class the method was found,
and which has an additional implicit self
argument.ApplyMethod()
the value of this
is passed as
an additional argument.PScm/Class.pm
1 package PScm::Class; 2 3 use strict; 4 use warnings; 5 use base qw(PScm); 6 7 sub new { 8 my ($class, $parent, $fields, $methods, $env) = @_; 9 10 my $rh_methods = {}; 11 $class->_populate_methods_hash($rh_methods, $methods); 12 13 return bless { 14 parent => $parent, 15 fields => [$fields->value], 16 methods => $rh_methods, 17 env => $env, 18 }, $class; 19 } 20 21 sub _populate_methods_hash { 22 my ($class, $rh_methods, $methods) = @_; 23 if ($methods->is_pair) { 24 my $method = $methods->first; 25 my ($name, $args, $body) = $method->value; 26 $rh_methods->{ $name->value } = 27 { args => $args, body => $body }; 28 $class->_populate_methods_hash($rh_methods, $methods->rest); 29 } 30 } 31 32 sub Apply { 33 my ($self, $form, $env) = @_; 34 35 my $new_object = $self->make_instance(); 36 $new_object->call_method($new_object, "init", $form, $env); 37 return $new_object; 38 } 39 40 sub make_instance { 41 my ($self) = @_; 42 43 my $parent_instance = $self->{parent}->make_instance(); 44 45 return $self->{env}->ExtendUnevaluated( 46 new PScm::Expr::List( 47 PScm::Expr::Symbol->new("class"), # $self 48 PScm::Expr::Symbol->new("super"), # $parent_instance 49 @{ $self->{fields} }, # 0... 50 ), 51 new PScm::Expr::List( 52 $self, # "class" 53 PScm::Env::Super->new(super => $parent_instance), # "super" 54 ((PScm::Expr::Number->new(0)) x @{ $self->{fields} }), # field... 55 ) 56 ); 57 } 58 59 sub get_method { 60 my ($self, $method_name, $object) = @_; 61 62 if (exists $self->{methods}{$method_name}) { 63 return PScm::Closure::Method->new( 64 $self->{methods}{$method_name}{args}, 65 $self->{methods}{$method_name}{body}, $object); 66 } 67 } 68 69 ########################## 70 package PScm::Class::Root; 71 72 use base qw(PScm::Class); 73 74 sub new { 75 my ($class, $env) = @_; 76 77 return bless { 78 parent => 0, 79 fields => [], 80 methods => {}, 81 env => $env, 82 }, $class; 83 } 84 85 sub make_instance { 86 my ($self) = @_; 87 88 return $self->{env} 89 ->ExtendUnevaluated( 90 new PScm::Expr::Symbol("class"), 91 $self 92 ); 93 } 94 95 1;
Since the new PScm::Class has a file to itself there's a full listing in Listing 22.
To recap, let's consider our original example classes: Account
and InterestAccount
Figure 12.3 shows the situation after the creation of
the Account
and InterestAccount
classes, and the
my-account
instance of an InterestAccount
that
was discussed in the examples in Section 12.1.
You can see that the my-account
object is really just a
PScm::Env and its parent env is the global environment
(implied by the unterninated heavy arrows.) The my-account
object's parent environment is the global environment because that is
the environment that the InterestAccount
class was created in.
If the InterestAccount
class had captured a different
environment, then that would have been the one that instances of that
class extended.
Note the three bindings in the my-account
object. The
rate
variable is the one supplied by the class definition,
the other two, super
and class
are automatically
provided by the implementation when new objects are created.
The super
variable refers to a PScm::Env::Super
object, derived from PScm::Env, which in turn has a super
variable, and differs from PScm::Env only in its Apply()
method, which arranges to forward the current value of this
(rather than the super
object itself) to the called method.
The class
variable refers to a PScm::Class object
which contains field (variable) names, method names along with their definitions,
the environment that was current at the time of the creation of the
PScm::Class, and a parent
field pointing at the
parent PScheme class.
t/PScm_OO.t
1 use strict; 2 use warnings; 3 use Test::More; 4 use lib 't/lib'; 5 use PScm::Test tests => 6; 6 7 BEGIN { use_ok('PScm') } 8 9 10 eval_ok(<<EOF, "account", 'classes'); 11 (define account 12 (make-class root (amount) 13 (init (x) (set! amount x)) 14 )) 15 EOF 16 17 eval_ok(<<EOF, <<EOR, 'objects'); 18 (define account 19 (make-class 20 root 21 (balance) 22 (init (x) (set! balance x)) 23 (balance () balance) 24 (withdraw (x) (set! balance (- balance x))) 25 )) 26 (define myaccount (account 10)) 27 (myaccount balance) 28 (myaccount withdraw 2) 29 (myaccount balance) 30 EOF 31 account 32 myaccount 33 10 34 8 35 8 36 EOR 37 38 eval_ok(<<EOF, <<EOR, 'inheritance'); 39 (define account 40 (make-class 41 root 42 (balance) 43 (init (x) (set! balance x)) 44 (balance () balance) 45 (withdraw (x) (set! balance (- balance x))) 46 (deposit (x) (set! balance (+ balance x))) 47 )) 48 (define interest-account 49 (make-class 50 account 51 (rate) 52 (init (x r) 53 (begin 54 (super init x) 55 (set! rate r))) 56 (accumulate () 57 (this deposit (* (this balance) rate))) 58 )) 59 (define myaccount (interest-account 10 2)) 60 (myaccount balance) 61 (myaccount withdraw 2) 62 (myaccount balance) 63 (myaccount accumulate) 64 (myaccount balance) 65 EOF 66 account 67 interest-account 68 myaccount 69 10 70 8 71 8 72 24 73 24 74 EOR 75 76 eval_ok(<<EOF, <<EOR, 'class variables'); 77 (define counter-class 78 (let ((count 0)) 79 (make-class 80 root 81 () 82 (init () (set! count (+ count 1))) 83 (count () count) 84 ))) 85 (define o1 (counter-class)) 86 (o1 count) 87 (let ((o2 (counter-class)) 88 (o3 (counter-class))) 89 (o1 count)) 90 EOF 91 counter-class 92 o1 93 1 94 3 95 EOR 96 97 eval_ok(<<EOF, <<EOR, 'super calls'); 98 (define c1 99 (make-class 100 root 101 () 102 (ma () (this mb)) 103 (mb () 0) 104 )) 105 (define c2 106 (make-class 107 c1 108 () 109 (ma () (super ma)) 110 (mb () 1) 111 )) 112 ((c2) ma) 113 EOF 114 c1 115 c2 116 1 117 EOR 118 119 # vim: ft=perl
Tests for our OO extension are in Listing 23.
The first test exercizes the creation of a class. The second test
creates a class (our account
class from the examples
above) then creates an object from it and calls a couple of its
methods. The third test uses the interest-account
example that we've looked at to test inheritance. The fourth test
demonstrates that lexical variables outside of a class are visible
to its methods and can therefore be used as class variables.
Finally, the fifth test uses an abstract form of that “leo
”
example to demonstrate that method calls on a super
object persist the current value of this
.
Full source code for this version of the interpreter is available athttp://billhails.net/Book/releases/PScm-0.0.9.tgz
If you
don't know SmallTalk, you might be surprised at how far that statement
goes. Not only are the simple numeric and string data types objects,
but arrays, hashes (called Dictionaries), booleans, code blocks,
exceptions and even classes are objects in SmallTalk. Furthermore
even the simplest operations such as addition are methods: adding
2 + 2
involves sending the object 2
the message
+
with argument 2
, and conditional expressions
like if
are implemented by sending a boolean object
representing the condition a message ifTrue
with argument the
code block to execute if the condition is true. See [sttl]
if you want more information.
We could have chosen the name self
instead, to make the examples easier for perl programmers to read,
but the perl under the hood might start to get ugly.
It's actually redundant for that root environment to
have a class
binding or a parent environment, since the
root class currently has no methods. However if we did want to
extend the implementation to add generic methods to the root class
then all the pieces we need are in place, so we can accept that
redundancy for now.