OADL classes are similar to C structs and C++ classes, but there are some important differences. Unlike C structs (but like C++ classes), OADL classes allow methods (public procedures) to be defined. Unlike C++ classes, though, all methods are essentially virtual.
Also like C++, classes may define properties which are either private (the default - not visible to other classes or external functions) or public. Because of OADL's dynamic typing, it is not necessary to include a complete class definition in a header file in order to use it or subclass it.
To declare a class (without defining it), use the class declaration:
class namelist;
To fully define a class and its attribute, use the class definition:
class name { proplist }
The proplist is a sequence of constant, variable, and procedure definitions. See the OADL Program Syntax for a description of the syntax.
Only the public properties are visible outside the class. Public
properties are declared using the public
keyword:
public const constlist public var varlist public proc proc
Public properties may also be defined outside a class; however, the default association of a public property with an actual value can only be done inside a class definition. The syntax for declaring a public property or properties is:
public namelist;
There is not necessarily any association of those public
properties with any particular class; this is simply a forward
declaration of properties that might be part of some class.
Referencing a non-existent public property of a class or object
results in the value nil
. Any attempt to assign a
value to non-existent public property results in a
RangeCheck
exception.
A subset of public properties exist in OADL; these are protected properties. Protected properties may be referenced outside their class definition, just like a public property. However, they may only be assigned a new value inside a class-public or class-private procedure. The syntax is very similar to the syntax for public properties:
protected var varlist
Although OADL accepts the protected
keyword
for const
and proc
declarations, in those cases, the effect is exactly the same as the
public
keyword.
The public
names exist in a separate namespace
from other program objects. OADL attempts to use the proper namespace
based on context, but the program may override its choice by using
the public
keyword as the namespace in an
expression:
proc create() { "pubname(create) = ", pubname(public::create), "\n"; }
The private elements of a class are those elements which are not
visible or accessible to outside procedures. To declare private
elements in a class, merely insert a const
,
var
, or proc
declaration
inside the class definition scope; for example:
class box { const MyName = "Boxy"; var position, size; proc PrintMyName() { say(MyName); } }
Private elements are very useful if it is necessary to change the underlying implementation of a class. Since no other procedure can even know that private data exists in a class, changing that private data can in no way affect those other procedures. References to private elements inside the class declaration look exactly like normal variable or constant references; the compiler keeps track of what is meant by each name.
Public constants are constants which are visible to the outside world, but not modifiable. For example, suppose that boxes, by default have a weight which is meaningful for other objects to refer to:
class box { public const Weight = 60; }
One special public constant is defined for all objects and classes
in OADL: parent
. It refers to the parent class of
a class or object, and is handy when a class wants to enhance a
public procedure with some extra code, but wants to use the parent
same public procedure of the parent class as well to do something.
Other procedures can refer to the public constant
parent
switch (obj.parent) { case box : /* Do something boxy */ case sphere : /* Do something spherical */ /* and so on */ }
A class which was created without a parent has a parent public
constant of nil
.
Public variables are variables which are visible to and modifiable by outside procedures. If, for example, outside physical forces might move a box around without having to call a public box method, its position could be declared as a public variable:
class box { public var pos = [0,0]; }
Be very careful, though, when using public variables: the variable is exposed effectively as an interface to the class. This might cause trouble later if the class definition is changed. This risk may be somewhat reduced by annotating the public variable with a type decoration:
class box { public var pos : PackInt[2] = [0,0]; } box box1() box1.pos = [1,2] box1.pos = nil Illegal type // OADL does type conversions for conformant array assignments box1.pos = [1.2, 2.2] box1.pos 1 2
Protected variables are variables which are visible to outside procedures, but only modifiable by class-private and class-public procedures. If, for example, the temperature of an object may slowly approach room temperature in a well-defined way, but arbitrary code must not be allowed to change the temperature, a protected variable should be used:
class thermal { protected var temperature = 0; public proc create(initTemp) { temperature = initTemp; } public proc warmup(degrees) { temperature += degrees; } }
In this example, the create()
method modifies
the temp variable; however, any attempt by an outside
procedure to modify temp will result in a runtime
AccessCheck
error.
Note that a class may implement the assign operator
":=
" which, if provided, will be called
when an attempt is made to assign to a protected
variable. For data integrity reasons, the assign operator may only be
implemented if all parent classes of a class also implement it:
// Demonstrate implemenation of an assign operator class assign { protected var a, b; operator := (prop, val) { switch (prop) { case public::a : self.(prop) = val; default : throw oadl::AccessCheck; } } assign x() x.a = 10 x.b = 20 Access failure // Demonstrate restriction on overriding the assign // operator to gain access to protected variables class foo { protected var a; } class bar(foo) { operator := (prop, val) {} Access failure
Public procedures are also known as methods. They are procedures which execute as if they were inside the class: they have access to all internal class constants, variables, and procedures. The better way to do the box position example above is to use a public procedure to move the box, so that its internal representation might change later:
class box { var pos = [0,0]; public proc move(x, y) { pos[0] = x; pos[1] = y; } }
The token ->
may be used as a synonym for
the .
operator. This is useful when lexical
confusion might otherwise occur (for example, when an integer is the
target of the method call):
$ cat foo.oad proc main() { var a = 3.iterate(); } $ oadl foo.oad File foo.oad line 3: ';' expected var a = 3.iterate(); ---------------------^ $ cat bar.oad proc main() { // Use the -> operator to avoid confusion between a floating-point // constant and the method call var a = 3->iterate(); "", a, '\n'; } $ oadl bar.oad 0 1 2
All classes in OADL have two predefined methods,
create()
and destroy()
, and
one predefined operator, the completion operator {
}
. The create
method is called when a
dynamic object is created. The destroy
method is
called when OADL detects that no more references to an object exist,
and is about to free that object (see the section on Zombie Objects
for more details). The completion operator is called at the
completion of static public variable initialiation (see below). The
completion operator is also called at the completion of
with
statements (see the section on
with
statements in the Procedures chapter for more information).
The completion operator is called with several arguments. The
first argument is either false
(if the object is
only partially completed due to use of the ...
syntax) or true
. The rest of the arguments are
all of the public
names that were assigned inside
the current brace pair. For example:
class a { public var b; operator {} (compl) { using namespace oadl; var i; for (i = 1; i < nargs(); i++) { "Public ", arg(i), " was set to ", self.(arg(i)), "\n"; } if (compl) "Completed.\n"; } } a() aa { b = "foo" } Public b was set to foo Completed.
The global pseudo-constant self
refers to the
current object that the current topmost method call applies to; this
is handy if an object needs to reference itself to another routine.
For example:
proc ack() { "self = ", oadl::objname(self), "\n"; } class foo { public proc create() { ack(); } } foo() bar self = bar
Typically, classes do not need to provide their own
destroy()
implementation since the default one is
generally sufficient. The main reason a class might have a
destroy()
method is if the class holds references
to objects which are not tracked by OADL (for example, operating
system objects).
OADL, like many other object-oriented languages, supports operator
overloading. This allows classes to implement their own versions of
standard OADL operators. All operators except logical AND
&&
, logical OR ||
,
and object public property reference .
are
overloadable. A class overloads an operator via the
operator
keyword, thus:
class complex { public var real, imag; public proc create(r, i) { real = r; imag = i; } operator + (rhs) { return new complex(real + rhs.real, imag + rhs.imag); } operator \+ (lhs) { return new complex(lhs.real + real, lhs.imag + imag); } }
Binary operators are passed one argument - the other operand in the expression. If it is neccessary to distinguish the case where an object is the left operand from the case where an object is the right operand, "right-binding" versions of the operators are used. The names of each "right-binding" operator is the same as the name of the corresponding "left-binding" operator, but with a backslash in front of the operator:
\| |
\^ |
\& |
\< |
\> |
\+ |
\- |
\* |
\/ |
\% |
\== |
\!= |
\<= |
\>= |
\<< |
\>> |
\=> |
\~= |
\** |
|
|
If no "right-binding" operator is present, both left- and right-operand overloaded operators call the normal operator method.
Unary operators (~
and -
)
are passed no arguments. Since the minus operator
-
is both a binary and a unary operator, a
special operator name is provided for the unary version of minus.
This name is !-
. For example:
operator - (rhs) { return new complex(real - rhs.real, imag - rhs.imag); } operator \- (lhs) { // Right-binding version return new complex(lhs.real - real, lhs.imag - imag); } operator !- () () // Unary version return new complex(-real, -imag); }
It is possible to call an overloaded operator method without using
the default expression syntax. The name of an overloaded operator is
operator
op, where op is one of the
possible overloaded operators. Since the !
operator can natively operate on expressions of any type, the
"operator !
" syntax is the only way
this operator may be overloaded. As a shorthand, the backtick may be
used instead:
class factorial { operator ! (lhs) { if (lhs > 1) { return lhs * self.`!(lhs-1); } else { return lhs; } } } a = new factorial() "7! = ", a.`!(7), '\n' 7! = 5040 }
Procedure call overloading is a little different than general
operator overloading. Instead of an explicit list of arguments the
nargs()
and arg()
intrinsics
must be used to access the arguments of the overloaded procedure:
class procClass { public operator () () { var i; for(i = 0; i < oadl::nargs(); i++) { "arg(",i,") = ", oadl::arg(i), '\n'; } } } a = new procClass() a(1,2,3) arg(0) = 1 arg(1) = 2 arg(2) = 3
Array indexing overloading has two operators involved - index
references, and indexed assignments. Therefore, there are two
different operators: []
and
[=]
. The index reference operator
[]
works as might be expected - the argument list
is the list of indexes to be looked up. The index assignment operator
[=]
takes at least two arguments. The
last argument is the value that should be assigned. The
remaining arguments are the multi-dimensional index:
class arrClass { var arr; public proc create(n,m) { arr = [n,m].iterate(); } public operator [] (i0,i1) { return arr[i0,i1]; } public operator [=] (i0,i1,val) { arr[i0,i1] = val; } public proc print() { "", arr, '\n'; } } arrClass arr(4,5) arr[2,3] = 100 arr[2,3] 100 arr.print() 0 1 2 3 4 5 6 7 8 9 10 11 12 100 14 15 16 17 18 19
The flattened array index operators #[]
and
#[=]
are supported in a similar way.
OADL fully supports class inheritance. Inheritance creates a new class derived from an existing one. The new class inherits all of its private constants, variables, and procedures, as well as all of its public properties. The new subclass may then change or add to that list to customize itself. To declare a class which inherits from another class, use the following syntax:
class name( parent ) { proplist }
The proplist may change the initial value of variables,
constants, and public properties, and it may add new ones. The
proplist may not change an element to an element of a
different kind; for example, the program may not change a
const
to a var
, or a public
property to a private element. Here is an example of inheritance:
class PhysObj { var Pos = { 0, 0, 0 }, Mass = 1.0, Momementum = { 0, 0, 0 }; public proc Accelerate( force ) { Momentum[0] += force[0]; Momentum[1] += force[1]; Momentum[2] += force[2]; } public proc Move( dT ) { Pos[0] += dT * Momentum[0] / Mass; Pos[1] += dT * Momentum[1] / Mass; Pos[2] += dT * Momentum[2] / Mass; } } class SphereObj( PhysObj ) { var Radius = 1.0; public proc create( radius ) { Radius = radius; } }
The SphereObj is a subclass of PhysObj, and has all of its
properties, including Pos, Mass, and Momentum, and all of its public
properties. It defines a new property, Radius, and its
create()
method is called with the desired
radius.
To solve certain problems (for example, in an adventure, to define
a magical object which provides light and unlocks a door), it is
often useful to have a class which is derived from more than one
parent class. OADL supports a multiple inheritance capability which
helps when these issues come up. To declare a class which inherits
from multiple parents, simply list all of the parents in the
class
declaration, thus:
class litObjClass { public const lit = 1; } class keyObjClass { public const unlocks = 1; } class litKeyClass(litObjClass,keyObjClass) {}
In the example given, the class "litKeyClass" has the public and protected variables and constants from both litObjClass AND keyObjClass.
When creating classes with multiple inheritance, properties from
parent classes later in the list of parent classes override those of
earlier classes (except for the parent
public
property). For example:
class Aclass { var a = 1; public proc Describe() { "a = ", a, "\n"; } } class Bclass { var b = 2; public proc Describe() { "b = ", b, "\n"; } } class ABclass(Aclass,Bclass) {} ABclass ab {} ab.Describe() b = 2
In this example, the Describe public property from class Aclass was overidden by the Describe public property from Bclass. The reason for this is that, conceptually, inheriting from multiple classes is similar to inheriting from the first class in the list, and then adding the extra declarations. For example, the code above is very similar to this code:
class Aclass { var a = 1; public proc Describe() { "a = ", a, "\n"; } } class ABclass(Aclass) { var b = 2; public proc Describe() { "b = ", b, "\n"; } } ABclass ab {} ab.Describe() b = 2
The difference between this and multiple inheritance is that the Aclass and Bclass classes still exist as part of the ABclass. The following program is very hard to write with single inheritance, since the var a is in both Aclass and Bclass:
class Aclass { var a = 1; public proc aDescribe() { "a = ", a, "\n"; } } class Bclass { var a = 2; public proc bDescribe() { "a = ", a, "\n"; } } class ABclass(Aclass,Bclass) {} ABclass ab {} ab.aDescribe() a = 1 ab.bDescribe() a = 2
Classes may be created dynamically at runtime. These classes do
not have an inheritance tree - they always have a
parent
property of nil
. A
dynamic runtime class is created via the new
Class(
className, pubPropList)
syntax. The className is the name of the new class. The
pubPropList is a List
of pub, value
pairs. Each of the Public
pub items is
made a new public var
of the new class. The
corresponding value is used as the initial value of that
variable in the class.
Accomanying dyamic runtime classes are dynamic runtime publics.
These are created via the new
Public(
pubName)
syntax. The
pubName is the name of the new public.
An example of both dynamic publics and dynamic classes is:
public pub2; proc main() { // Create a couple of new publics var pub0 = new Public("pub0"); var pub1 = new Public("pub1"); // Create a new class using those, plus an existing public var cls = new Class("cls", {pub0, 123, pub1, "hello", pub2, 3.14}); "New class:\n"; forall (cls.(pub)) { "", pub, ": ", cls.(pub), '\n'; } "\n"; // Note that classes created with "new Class" are *not* readonly cls.(pub0) = 456; // Create an instance of that class var obj = new cls(); "New instance:\n"; forall (obj.(pub)) { "", pub, ": ", obj.(pub), '\n'; } }
The program above produces the following output:
New class: parent: nil pub2: 3.14 pub0: 123 pub1: hello New instance: parent: cls pub2: 3.14 pub0: 456 pub1: hello
Dynamic classes are garbage collected when all references to them are removed. However, dynamic publics are not; they persist for the duration of program execution.
Back to Arrays and Dictionaries