↑ Up |
As a first example, a class with a method quack
is stated.
class Duck = { function quack() print(self.name, " quacks.") end } d = table Duck{name = "Dagobert"} d.quack()
Object properties shadow class properties. For example,
the method quack
may be shadowed by a
special method:
d.quack = fn|| print(self.name, " quacks loudly.") end d.quack()
Objects are constructed by ordinary functions.
For example, a function duck
may create
an object of class Duck
.
function duck(name) return table Duck{name = name} end d = duck("Dagobert") d.quack()
It can be moved into the class, that is only a matter of style:
class Duck = { function quack() print(self.name, " quacks.") end, function new(name) return table Duck{name = name} end } d = Duck.new("Dagobert") d.quack()
When an object is not needed anymore, a destructor method
drop
may be called automatically. It is a special method
that can only be stated at class construction and cannot
be shadowed by local properties.
class Duck = { function quack() print(self.name, " quacks.") end, function drop() print(self.name, " flies away.") end }
Type checks are performed in two ways.
> type(d) is Duck true > d: Duck true
The former is only true if the types match exactly.
The latter is also true if d
is an descendant.
For example, d: Bird
would also be true,
in case Duck
inherits from Bird
.
Of course, type checks are also provided for the basic data types
Bool, Int, Long, Float, Complex,
Range, List, Map, String, Function.
The types Range
, List
, Map
,
String
, Function
inherit
from Iterable
.
Runtime type checks return a boolean value. Assert statements stay quiet for true and turn false into an exception. Any expression with boolean value is possible:
assert d: Duck assert x: Int and x>=0
A class may inherit properties from a parent class.
class Bird = { function fly() print(self.name, " flies.") end } class Duck: Bird = { function quack() print(self.name, " quacks.") end } d = table Duck{name = "Dagobert"} d.fly() d.quack()
As Duck just points to Bird, changing Bird afterwards will be reflected in Duck. This may be prevented by swallowing the properties of Bird instead. Replace
class Duck: Bird = {...}
by shallow copy:
class Duck = record(Bird)|{...}
or by deep copy:
class Duck = deep_copy(record(Bird))|{...}
The function deep_copy
is not part of the
core language. You will find it in the standard library.
A class may inherit from more than one parent.
class Diver = { function dive() print(self.name, " dives.") end } class Bird = { function fly() print(self.name, " flies.") end } class Duck: [Bird, Diver] = { function quack() print(self.name, " quacks.") end } d = table Duck{name = "Dagobert"} d.fly() d.dive() d.quack()
A property is first searched in Duck, then in Bird and then in Diver.
Instead of pointing to Bird and Diver, we can alternatively create a shallow copy:
function extends(*a) buffer = {} for m in a buffer.extend(record(m)) end return fn|m| buffer.update(m) return buffer end end class Diver = { function dive() print(self.name, " dives.") end } class Bird = { function fly() print(self.name, " flies.") end } class Duck = extends(Bird,Diver){ function quack() print(self.name, " quacks.") end } d = table Duck{name = "Dagobert"} d.fly() d.dive() d.quack()
An instance object may inherit from another instance object, its prototype. For example, a duck object may be regarded as an descendant of a bird object.
bird = table{ function fly() print(self.name, " flies.") end, name = "Unnamed bird" } d = table bird{name = "Dagobert"} bird.fly() d.fly()
The prototype system also allows multiple inheritance:
diver = table{ function dive() print(self.name, " dives.") end, name = "Unnamed diver" } d = table[bird, diver]{name = "Dagobert"} d.fly() d.dive()
Usage of the class system and the prototype system may be mixed:
class Bird = { function fly() print(self.name, " flies.") end } bird = table Bird{name = "Unnamed bird"} d = table bird{name = "Dagobert"} bird.fly() d.fly() assert d: Bird assert d: bird assert type(d) is bird
It should be noted that type
only returns the flat
type. To obtain the deeper underlying class, the more advanced behavior
shown in deep_type
is necessary.
function deep_type(t) T = type(t) return T if type(T) is Type else deep_type(T) end
Inheritance has the disadvantage that it might lead to name clashes. In case both ancestors have a method with the same name, which one should be used? This problem is solved by replacing inheritance with composition.
The following approach enables automatic composition by prefixing class properties.
function compose(tab) buffer = {} for prefix, type in tab.items() for key, value in record(type).items() buffer[prefix+"_"+key] = value end end return fn|m| buffer.update(m) return buffer end end class Diver = { function dive() print(self.name, " dives.") end } class Bird = { function fly() print(self.name, " flies.") end } class Duck = compose{"bird": Bird, "diver": Diver}{ function quack() print(self.name, " quacks.") end } d = table Duck{name = "Dagobert"} d.bird_fly() d.diver_dive() d.quack()
It might be that not only class properties clash, but also object properties. To solve this problem, we need more fine-grained control.
class Diver = { function dive() print(self.name, " dives ", self.quickness, ".") end } class Bird = { function fly() print(self.name, " flies ", self.quickness, ".") end } class Duck = { function quack() print(self.name, " quacks.") end } function duck(argm) name = argm["name"] bq = argm["bird_quickness"] dq = argm["diver_quickness"] return table Duck{ name = name, bird = table Bird{name = name, quickness = bq}, diver = table Diver{name = name, quickness = dq} } end d = duck{ name = "Dagobert", bird_quickness = "fast", diver_quickness = "very fast" } d.bird.fly() d.diver.dive() d.quack()
Reflection is the possibility to construct, obtain and modify the structure of a type at runtime. To achieve this, classes and objects may constructed dynamically from data.
Duck = class "Duck" { "quack": fn|| print(self.("name"), " quacks.") end } d = table Duck{"name": "Dagobert"} d.("quack")()
The properties are ordinary key-value maps:
class_data = { "quack": fn|| print(self.("name"), " quacks.") end } Duck = class "Duck" (class_data) table_data = {"name": "Dagobert"} d = table Duck(table_data) d.("quack")()
The map may be accessed with record
:
m = record(Duck) m["quack"](d;)
We have:
d.name == d.("name") == record(d)["name"]
But note that the dot operation also searches in the class and its ancestors, whereas the record lookup does not.
A class may be modified automatically in such a way, that
a special function called an advice enables us to do operations
before and after each method call. We will call this wrapping.
For example, a function log
should print to the
command-line when a method starts and ends.
function wrap(Class,advice) m = record(Class) for key, value in m.items() if type(value) is Function m[key] = |*argv| advice(key,value,self,argv) end end end function log(name,method,pself,argv) print("[Start of method {}]"%[name]) rv = method(pself;*argv) print("[End of method {}]"%[name]) return rv end class Duck = { function quack() print(self.name, " quacks.") end } wrap(Duck,log) d = table Duck{name = "Dagobert"} d.quack()
Table of binary overloadable operators:
Op. | left | right |
---|---|---|
a+b
| add
| radd
|
a-b
| sub
| rsub
|
a*b
| mul
| rmul
|
a/b
| div
| rdiv
|
a//b
| idiv
| ridiv
|
a^b
| pow
| rpow
|
a&b
| band
| rband
|
a|b
| bor
| rbor
|
a==b
| eq
| req
|
a<b
| lt
| rlt
|
a<=b
| le
| rle
|
Table of unary overloadable operators:
Op. | Method |
---|---|
-a
| neg
|
~a
| comp
|
Here is an implementation of complex number arithmetic. Complex numbers are covered already by the Moss language, but this implementation allows also for arithmetic of complex integers.
function complex(x,y) return table Complex{re=x, im=y} end class Complex = { function string() return "({}, {})" % [self.re, self.im] end, function neg() return table Complex{re = -self.re, im = -self.im} end, function add(a;b) if b: Complex return table Complex{re = a.re+b.re, im = a.im+b.im} else return table Complex{re = a.re+b, im = a.im} end end, function radd(a;b) return table Complex{re = a+b.re, im = b.im} end, function sub(a;b) if b: Complex return table Complex{re = a.re-b.re, im = a.im-b.im} else return table Complex{re = a.re-b, im = a.im} end end, function rsub(a;b) return table Complex{re = a-b.re, im = -b.im} end, function mul(a;b) if b: Complex return table Complex{ re = a.re*b.re-a.im*b.im, im = a.re*b.im+a.im*b.re } else return table Complex{re = a.re*b, im = a.im*b} end end, function rmul(a;b) return table Complex{re = a*b.re, im = a*b.im} end, function div(a;b) if b: Complex r2 = b.re*b.re+b.im*b.im return table Complex{ re = (a.re*b.re+a.im*b.im)/r2, im = (a.im*b.re-a.re*b.im)/r2 } else return table Complex{re = a.re/b, im = a.im/b} end end, function rdiv(a;b) r2 = b.re*b.re+b.im*b.im return table Complex{ re = a*b.re/r2, im = -a*b.im/r2 } end, function pow(a;n) return (1..n).prod(|k| a) end }
Example of use:
> i = complex(0,1) > 4+2*i (4, 2) > (4+2*i)*(5+3*i) (14, 22) > (4+2*i)^60 (-964275081903216557328422924784146317312, 472329409445772258729579365571477110784) > (4+2i)^60 -9.64275e+38+4.72329e+38i
Sometimes only methods that belong to the object should have write access to a property. Such a close relationship of the object to its methods can be achieved by a closure-binding of an interal private property table.
class Duck = {} function duck() private_tab = {beak_color = "yellow"} return table Duck{ function get(property) return private_tab[property] end, function orange_beak() private_tab["beak_color"] = "orange" end } end d = duck() print(d.get("beak_color")) d.orange_beak() print(d.get("beak_color"))
Every method that has direct access to private_tab
must belong to d
and not to its
type Duck
.
Private properties are constructed the same way as read-only properties. Only methods that belong to the object should be able to see the properties.
class Duck = {} function duck() private_tab = {beak_color = "yellow"} return table Duck{ function quack() color = private_tab["beak_color"] print("The duck quacks out of his {} beak."%[color]) end, function orange_beak() private_tab["beak_color"] = "orange" end } end d = duck() d.quack() d.orange_beak() d.quack()
We are able to add methods to already existent types. This technique is called monkey patching and considered a bad practice, because it can result in name conflicts.
As an example, we will add a method to the list type that splits the list into pairs.
> List.pairs = || list(self.chunks(2)) > list(1..4).pairs() [[1,2], [3,4]]
A more interesting example of a convenience extension:
String.split = fn|sep| a = []; w = [] for c in self if c in sep if len(w)!=0 then a.push(w.join()) end w = [] else w.push(c) end end if len(w)!=0 then a.push(w.join()) end return a end
> "1, 12, 3.5, 9".split(",\s") ["1", "12", "3.5", "9"]
Custom class constructors allow us to state object systems with different behavior.
# An implementation of prototyping inheritance. function get(key) data = record(self) while true if key in data return data[key] elif "prototype" in data data = record(data["prototype"]) else raise "key '{}' not found" % [key] end end end function set(key,value) record(self)[key] = value end # A custom class constructor. function pclass(name,m) m["get"] = get m["set"] = set return class(name)(m) end Object = pclass("Object",{}) bird = table Object{ function fly() print(self.name, " flies.") end, name = "Unnamed bird" } d = table Object{prototype = bird, name = "Dagobert"} bird.fly() d.fly()