Object system

Table of contents

  1. Construction
  2. Object literal
  3. Prototypes
  4. Basic data types
  5. Classes and Metaclasses
  6. Multiple inheritance
  7. Reflection
  8. Monkey patching
  9. Operator overloading
  10. Read-only properties
  11. Private properties

Construction

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.

Object literal

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)

Prototypes

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

Basic data types

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

Classes and Metaclasses

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

Multiple inheritance

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

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)

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]]

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

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

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.

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

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