Object system

Table of contents

  1. A simple class
  2. Constructors
  3. Destructors
  4. Runtime type checks
  5. Inheritance
  6. Multiple inheritance
  7. Prototype based inheritance
  8. Composition over inheritance
  9. Reflection
  10. Aspect-oriented programming
  11. Operator overloading
  12. Read-only properties
  13. Private properties
  14. Monkey patching
  15. Custom class constructors

A simple class

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()

Constructors

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()

Destructors

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
}

Runtime type checks

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

Inheritance

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.

Multiple inheritance

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()

Prototype based inheritance

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

Composition over inheritance

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

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.

Aspect-oriented programming

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()

Operator overloading

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

Read-only properties

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

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()

Monkey patching

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

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()