Starting with version 3.00, m offers the following OOP features:
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 |
|
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:
|
x:Sum=null |
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 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
|
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:
|
s:Sum=... s.s=0 |
|
sums=[...] print sums[3].(Sum)s // accessing s requires a cast |
|
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:
|
s:Sum=... s.add(3) // call add on s |
|
sums=[...] sums[3].(Sum)add(4) // requires a cast to Sum |
|
class Sum ... function addtwice(x) // same as this.add(x); this.add(x) add(x); add(x) end end |
|
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.
|
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:
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 ... |
|
// 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.
|
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:
|
x:.Instance=.Instance() |
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
s:Sum=Sum()-8 9 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).
|