Prev: Exceptions
Object Oriented Programming
Starting with version 3.00, m offers the following OOP features:
- Classes with fields and (virtual) functions.
- Single inheritance to build class hierarchies.
- Creation of class instances (objects) and initialization with constructor functions.
- Polymorphism through function overriding.
- Instance function references providing a callback mechanism for listeners or event handlers ("delegates").
Classes and Class Instances
A class is a declaration of related variables (fields) and functions ("methods") operating on them. A class only declares a type or pattern; the data is created by creating class instances.
Classes can be based on existing classes, inheriting all their fields and functions. Inherited functions can be overwritten to change the behaviour or functionality offered by the class.
A class is declared by the keyword class followed by the class name, its fields and its functions, followed by the keyword end:
class Sum
s
function add(x)
s+=x
end
function res()
return s
end
end
|
This declares a class Sum with a field s, and two functions: one to add a value x to s, the other to return the sum of all added values.
A class always belongs to the module declaring it (which can be the builtin module or main script). A class is hence uniquely identified by the module declaring it and its name within the module: if class Sum is declared in module Aggreg, it must be referenced as Aggreg.Sum (or with the corresponding alias of Aggreg) in other modules.
Classes must be declared before they can be used. This means that if two classes reference each other, at least one must be declared with forward and defined later. In the following example, either C or D must be forward declared, since class C references class D and vice versa:
class C forward // make C known, without any details
class D
x: C // C can be used, but C.y is not yet visible
end
class C // define C
y
function f(d: D)
return y*d.x.y
end
end
|
| | |
ClassIdentifier := [ModulePrefix] Identifier .
ClassDeclaration := class Identifier
( forward | [ is ClassIdentifier ] ClassBody end ) .
ClassBody := { VariableDeclaration | FunctionDeclaration [';'] } .
|
|
Variable Declarations, revisited
A variable can be declared to always reference an instance of a given class (or to be null). This allows to directly access the fields and functions of the instance. For instance, to declare a variable x referencing an instance of Sum, follow the first assignment (i.e. the "declaration") of x by a colon and the class it references:
A variable cannot be redeclared, or declared lazily: the first assignment occuring in the source must declare its type, or it remains of undeclared type (like an ordinary m variable).
Whenever an expression is assigned to a variable of declared class, the value being assigned is checked. If it is not an instance of the declared class and not null, ExcNotSuchInstance is thrown:
x:Sum=null
a=23*7
x=a // a holds a number, not a Sum instance
→ ExcNotSuchInstance thrown
|
Function Declarations, revisited
Function parameters are like local variables, and can be declared to hold instances of a given class. For example, a function to multiply an instance s of Sum by a factor f and returning the resulting instance can be declared as follows:
function multiply(s: Sum, f): Sum
...
end
|
As with assignments to variables of declared class, the expressions assigned to the parameters are checked when calling the function, and the return value is checked when returning a value from the function:
multiply("no sum", 3)
→ ExcNotSuchInstance thrown
function getsum(): Sum
return "also no sum"
end
y=getsum()
→ ExcNotSuchInstance thrown
|
Class Fields
A useful class normally contains fields, i.e. variables which each class instance holds. Fields are declared by simply listing their name in the class body, optionally separated by semicolons. A field must be declared before it can be referenced in a function. When an instance is created, all its fields are initialized to null.
Class fields are accessed as follows:
- To access a class field of an instance variable with declared type, append a dot and the field name:
- To access a class field of an expression without declared type, it must be "casted" to the desired type before accessing any of its fields. ExcNotSuchInstance is thrown if the expression is not an instance of the desired type.
sums=[...]
print sums[3].(Sum)s // accessing s requires a cast
|
- Within a class function, class fields are directly accessible, as shown in functions add() and res() of class Sum: s can be used like any other variable.
| | |
InstanceSelector := '.' [ '(' ClassIdentifier ')' ]
(FieldIdentifier |
FunctionIdentifier '(' [ActualParameterList] ')') .
|
|
Class Functions
Most classes also contain functions. Class functions operate on an instance, i.e. its fields. For instance, the function add in class Sum adds a value to the field s of the instance.
Within a function of class C, the instance is accessible via the (predeclared) parameter-like variable this:C. Explicitly mentioning this to access an instance field may be required if there is a parameter of the same name:
class C
x
function setx(x)
this.x=x // assign parameter x to field x
end
end
|
The rules for calling class functions are the same as those on accessing class fields:
- To call a class function on an instance variable with declared type, append a dot and the function name:
s:Sum=...
s.add(3) // call add on s
|
- To call a class function on an expression without declared type, it must be "casted" to the desired type before calling any of its functions. ExcNotSuchInstance is thrown if the expression is not an instance of the desired type.
sums=[...]
sums[3].(Sum)add(4) // requires a cast to Sum
|
- Within a class function, another function of the class can be called directly, without first denoting the instance.
class Sum
...
function addtwice(x)
// same as this.add(x); this.add(x)
add(x); add(x)
end
end
|
There are two ways to define a class function: either directly within the class body, or by declaring it as
forward and then defining it outside the class, by prefixing the function name by the class identifier. The latter may be required when classes and their functions are referencing each other.
class C forward // make C known, without any details
class D
x: C // C can be used, but C.y is not yet visible
function mult(a) forward
end
class C // define C
y
end
function D.mult(a) // C is defined, now define D.mult
return x.y*a
end
|
The next section shows how class functions can be overwritten in subclasses.
| | |
ClassFunctionDeclaration := function ClassName '.' Identifier
FunctionBody .
|
|
Inheritance, Sub- and Superclasses
One of the key properties of classes is their extensibility: new classes can be declared based on existing classes, inheriting their fields and functions. By overriding functions, the behaviour of class instances can also be extended or modified.
To define a new class extending an existing class, append is and the existing class name after the new class identifier:
class Avg is Sum
n // element counter to calculate the average
function add(x) // overrides Sum.add()
s+=x; n++
end
function res() // overrides Sum.res()
return s/n
end
function count()
return n
end
end
|
This establishes the following simple class hierarchy:
- Avg is a subclass of Sum (each class can have many subclasses).
- Sum is the superclass of Avg (each class except .Instance has exactly one superclass).
Since
Avg extends
Sum, it inherits its field
s and its functions
add() and
res(). The functions are overwritten to implement averaging behaviour, and the extended class also gets a new function
count() returning the element count.
The functions of the superclass are always accessible by the builtin parameter super. It references the current instance like this, but seen an instance of the superclass when determining which function to call. Hence, the overriding functions in Avg could also be written as:
function add(x) // overrides Sum.add()
super.add(x); n++
end
function res() // overrides Sum.res()
return super.res()/n
end
|
By declaring class functions as forward without implementing them, abstract classes can be declared. For instance, an interface-like abstract class Aggregator could be the base class of all aggregating classes Sum and Avg:
class Aggregator
function add(x) forward
function res() forward
end
class Sum is Aggregator
...
|
Instance Creation and Constructors
Defining a class also defines a function of the same name, its creator function. Calling this function has the following effects:
- A new instance of the class is created.
- All fields of the class (and its superclasses) are set to null.
- The class constructor function init() of the new instance is called, with the parameters that were passed to the creator function.
- The new instance is returned.
// create a new Sum instance and assign it to x
x:Sum=Sum()
print x
→ .Sum(s=null)
|
Defining (overriding) the init() function of Sum, its field s can be properly initialized to zero:
class Sum
s
function init()
s=0
end
function add(x)
...
end
|
x:Sum=Sum()
print x
→ .Sum(s=0)
|
The init() function can take arbitrary parameters, and they can be different in number and type for each superclass:
class Person
name
height
function init(name="unknown",height=180)
this.name=name; this.height=height
end
end
print Person()
→ .Person(name=unknown,height=180)
print Person("Lucky Luke")
→ .Person(name=Lucky Luke,height=180)
print Person("Joe",155)
→ .Person(name=Joe,height=155)
|
Note that there are no destructor functions in m. Class instances which are no longer needed are automatically deleted by the garbage collector, without explicit cleanup.
| | |
InstanceCreation := ClassIdentifier '(' [ActualParameterList] ')' .
|
|
The .Instance Base Class
There is a single builtin class .Instance which is the implicit base class of all classes. It is declared as an empty class with empty constructor function:
class Instance
function init() end
end
|
The following two declarations are therefore equivalent
class Sum
...
end
class Sum is .Instance
...
end
|
Even though it generally makes little sense, it is perfectly valid to create an instance of .Instance:
Instance Function References
An instance function reference is like a function reference (see section * ()), but always operates on a given instance defined when obtaining the reference.
Instance function references are most useful to implement callbacks in an object oriented environment, for instance event listeners. Sometimes they are also called "delegates" or "delegate functions", since the function reference acts like a delegate of the instance passed to another instance.
Consider the following function passing values in array a to a consumer function c:
function consume(a, c)
for v in a do
c(v)
end
end
function out(n)
print n
end
consume([7,-8,9], &out) // ordinary function reference
→ 7
-8
9
s:Sum=Sum()
consume([7,-8,9], s.&add) // instance function reference
print s, s.res()
→ .Sum(s=8), 8
|
The second call to consume() calls s.add(v) on each call of c(v).
| | |
InstanceFunctionReference :=
'.' [ '(' ClassIdentifier ')' ] '&' Identifier .
|
|
Next: Source Structure© 2004-2011 airbit AG, CH-8008 Zürich
Document AB-M-REF-887