↑ Up |
We can take different approaches to construct an object. A simple object can be constructed in one of the following three ways or a mixture of them.
# imperative construction # vmax in kn Bristol = table{} Bristol.id = "HMS Bristol" Bristol.color = "gray blue" Bristol.engine = "diesel" Bristol.vmax = 25 # via object literal Bristol = table{ id = "HMS Bristol", color = "gray blue", engine = "diesel", vmax = 25 }
Such an object may have a method which operates on it. We will define one.
Bristol.move = fn|| print(self.id, " moves at ", self.vmax//2, " kn.") end Bristol.move() # HMS Bristol moves at 12 kn.
If there shall be more than one ship, there are certain options now. Firstly we can simply delegate to HMS Bristol and shadow some of the member objects.
Hermes = table Bristol{} Hermes.id = "HMS Hermes" Hermes.vmax = 25
But we might also clone HMS Bristol completely and then overwrite some of the member objects.
Hermes = table{} extend(Hermes,Bristol) Hermes.id = "HMS Hermes" Hermes.vmax = 25
Another way is to use a constructor ship
and a type/class/metaobject Ship
.
function ship(id,vmax) return table Ship{id=id, vmax=vmax} end Ship = table{ color = "gray blue", engine = "diesel", function move print(self.id, " moves at ", self.vmax//2 " kn.") end } Bristol = ship("HMS Bristol",25) Hermes = ship("HMS Hermes",25)
Frankly speaking, the metaobject Ship
can be
regarded as a table of virtual methods and class variables.
The following ways to construct Hermes have the same result.
Hermes = table Bristol{} Hermes.id = "HMS Hermes" Hermes.vmax = 25 Hermes = table Bristol{ id = "HMS Hermes", vmax = 24 } Hermes = table Bristol{ "id": "HMS Hermes", "vmax": 24 } m = {id = "HMS Hermes", vmax = 24} Hermes = table Bristol(m)
Every object x
has a prototype.
If some member object is not found in x
, it will be
looked up in the prototype of x
and then in the
prototype of the prototype of x
and so on.
In Moss, the type of an object and its prototype coincide, because the type of an object is determined by its prototype. The prototype stores the propertys and operations, that objects of some kind have in common.
> a = table{time="14:12"} # a is a new object with prototype null > b = table a{} # b is a new object with prototype a > type(b) is a true > type(null) is null true # null is its own prototype and is the only # object which shall have this property > b.time "14:12" # b does not have 'time', but a has it
There are two ways to determine, whether some object is of some type or not.
> T = table{} > S = table T{} > a = table S{} # T is not the direct prototype of a. > type(a) is T false > type(a) is S true # But a is an ancestor of T. > a: T true > a: S true # S is a subtype of T. > S: T true # S is a direct subtype of T. > type(S) is T true
If S
was extended by T
, we must
take a different approach.
> T = table{a=1, b=2} > S = table{c=3} > extend(S,T) # This won't work anymore. > S: T false # But one can test, whether all members of T # are contained in S. > record(T) < record(S) true
Variables of all basic data types as well as the data types itself are objects. Thus the object system applies to the basic data types too. Type checks are performed in one of two ways:
> x = 1 > x: Int true > type(x) is Int true
Here is a list of the basic data types:
Bool, Int, Long, Float, Complex, Range, List, Map, String, Function.
A data type is written capitalized, the corresponding constructor function in lower case.
> list(1..10): List true
To be honest, the coincidence between types and prototypes can be broken. A table constructor may take a tuple of three parts instead of a simple prototype. In general we have:
x = table(type, name, prototype) {}
Properties are searched in x
first and then
in the prototype chain. But type(x)
will lead
to the type, not the prototype. The type of a type is sometimes
called a metaclass. The metaclass of the basic data types
is called Type
. Thus we have:
> type([]) List > type(List) Type > prototype([]) List > prototype(List) Iterable
Now, basic class construction is stated as follows:
Bird = table(Type,"Bird"){ function string "{} is a bird." % [self.name] end, function fly "{} can fly!" % [self.name] end }) Duck = class(Type,"Duck",Bird){ function string "{} is a duck." % [self.name] end, function dive "{} can dive!" % [self.name] end }) d = table Duck{name = "Donald"} print(d) print(d.fly()) print(d.dive()) print("type: ", type(d)) # Output: # Donald is a duck. # Donald can fly! # Donald can dive! # type: Duck
An object can have more than one prototype.
Plane = table{ function take_off print(self.id, " is in the sky.") end } sea_duck = table[Ship, Plane]{ id = "Sea duck", vmax = 40 } sea_duck.move() # Sea duck moves at 20 kn. sea_duck.take_off() # Sea duck is in the sky.
A method is searched at first in Ship
,
and then, if not found, in Plane
.
Reflection is the possibility to construct, obtain and modify the structure of a type at runtime.
# direct construction Bristol = table{color = "gray blue", engine = "diesel"} # reflection x = "color"; y = "engine" Bristol = table{x: "gray blue", y: "diesel"} # reflection, by a custom map x = "color"; y = "engine" m = {x: "gray blue", y: "diesel"} Bristol = table null(m) # direct access > Bristol.engine "diesel" # reflection > Bristol.("engine") "diesel" # direct method call > Bristol.move() # reflection > Bristol.("move")() # inspect a type > m == record(Bristol)
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]]
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 Complex = table(Type,"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.
Ship = table{} function ship private_tab = {color = "blue"} return table Ship{ function get(property) return private_tab[property] end, function change_color private_tab["color"] = "green" end } end Bristol = ship() print(Bristol.get("color")) Bristol.change_color() print(Birstol.get("color"))
Every method that has direct access to private_tab
must belong to ship
and not to its
type Ship
.
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.
Ship = table{} function ship private_tab = {color = "blue"} return table Ship{ function info return "A {} ship." % private_tab["color"] end, function change_color private_tab["color"] = "green" end } end Bristol = ship() print(Bristol.info()) Bristol.change_color() print(Bristol.info())