Moon - TetraSource/OC-programs GitHub Wiki
About
Moon is an API that brings OOP based on Pythons OO to OC Lua 5.3. It uses a C3 linearization algorithm like python does and supports multiple inheritance therefore. Additionally, it is contains properties, operator overloading and metaclasses.
Before getting started
When using Moon you should already know how basic OO works (in dynamic typed languages). However, I will try my best to cover all major differences between a static typed and dynamic typed language with regard to Moon.
I also want to stress the difference between the purpose of the terms class, class instance and object in this documentation. While the first one can generate class instances, the latter can be a class as well as a class instance. Don't confuse these terms with the classes moon.Class
and moon.Object
.
Last, note that I use the camelCase for the names of variables, functions and class instances and PascalCase for the names of classes.
Getting Started
Attributes are supposed to be accessed like this always:
instance.attribute
class.attribute
Methods are supposed to be called like this always:
instance:method(...) -- equal to instance.method(instance, ...)
class:method(...) -- equal to class.method(class, ...)
New classes are defined like this:
local C = moon.class("C")
-- define static attributes
C.attribute = "world"
-- define methods
function C:getText()
return tostring(self.value) .. " " .. tostring(self.attribute)
end
-- define the constructor that is always called __init
function C:__init(value)
self.value = value
end
New class instances are created like this:
local c = C("hello")
print(c.attribute) -- world
print(c:getText()) -- hello world
And now an example with inheritance:
local B = moon.class("B", C)
-- overwrite text
function B:getText()
-- check out its documentation for more information about super.
return moon.super(B, self).getText(self):upper() .. "!"
end
local b = B("hello world") -- uses constructor of A
print(b:getText()) -- HELLO WORLD!
There are no private or protected attributes or methods. If you don't want something to be accessed externally, put a _
in front of it (e.g. object.name
becomes object._name
). Doing so is like saying: "Don't access this attribute/method except you really know what you are doing".
This approach counts on the programmer to not break anything by giving her/him the freedom to access all attributes. This might sound a little weird to you if you come from languages such as Java. However, it is really just another approach with more emphases on flexibility and trust in the programmer.
Features
Multiple inheritance and method resolution order
Moon uses a method resolution order (MRO) just like in Python or Perl 5.9.5 or higher. A MRO is a (linear) list that determines in which order the namespaces of the base classes of a class C
are checked when a lookup is performed on C
. You can access it with C.__mro
.
If you are already familiar with the C3 linearization you can skip the rest of the paragraph. Otherwise I try my best in explaining it here shortly.
With simple inheritance this is rather simple:
local A = moon.class("A", Object)
local B = moon.class("B", A)
print(table.unpack(B.__mro)) -- MRO is {B, A, Object}
However, when it comes to multiple inheritance we need some rules in order to determine a unambiguous MRO. Since this API uses a C3 linearization we have to consider three factors:
- the monotony: If a class C has the MRO that contains a class B prior to A, all classes that inherit from C have B prior to A in their MROs.
- the local precedence order: If a class B inherits from A, the MRO of B has to satisfy the MRO of A.
- the consistent extension: Equal combinations of derivations result in equal MROs. This is archived by always choosing the first possible class in the MRo of the first possible base class.
Here is a simple example that demonstrates how it works:
local A = moon.class("A", Object)
local B = moon.class("B", Object)
local C = moon.class("C", B, A)
print(table.unpack(C.__mro)) -- MRO is {C, B, A, Object}
local D = moon.class("D", Object)
local E = moon.class("E", D, B)
print(table.unpack(E.__mro)) -- MRO is {E, D, B, Object}
local F = moon.class("F", C, E)
print(table.unpack(F.__mro)) -- MRO is {F, C, E, D, B, A, Object}
Note that there are combinations of derivations that can not be linearized by this rules. In this case the class creator raises an error:
Cannot create a consistent method resolution order (MRO) for bases
Look here for a thorough example how complex inheritance trees are linearized.
Properties and data descriptors
In moon data descriptors are values that allow you to control how they are accessed when they are in an object (e.g. object.x = value; value = object.x
). Formally they are tables (so they can also be objects) that have a __get
or a __set
field (or both). Unlike Python they don't need to have both. The __get(self, object) -> result
field realizes reading access, the __set(self, object, value) -> nil
field writing access whereby self
is the data descriptor and object
the object of the class it is used in. Of course they are just called if they exist.
Moon already provides a data descriptor class namely Property
(capitalized since it is a class). A property is a mean to ensure data encapsulation that is more clear than the setter/getter approach. Besides, they can allow you to control the accessing of variables that used to be "public" without changing their old using code.
The constructor of Property
takes two arguments namely a getter function and an optional setter function. If the setter is omitted, an assignment to the property raises an error. Otherwise assignment leads to a call to the setter with the object and the value as parameter (setter(object, value)
). The returned value is ignored. Subscripting leads to a call to the getter with the object as parameter (getter(object)
) and returns its results. Here is an example that also uses the setter/getter approach:
local C = moon.class("C")
function C:__init(value)
self.value = value -- or self:setValue(value)
end
function C:getValue()
return self._value
end
function C:setValue(value)
-- Here you could sync with something,
-- check if the value is allowed, ...
self._value = value
end
-- You can also directly define the call to moon.Property
C.p = moon.Property(C.getValue, C.setValue)
local c = C(42)
print(c.value)
In this example it actually totally unnecessary to use a property but it is just for demonstration purposes.
A property decorator (@property
) as in Python is due to Lua syntax not supported though.
Operator overloading
Every object has a metatable that implements all possible metamethods. Each of them checks whether it can find a method that has the same name as the metamethod. If that's the case, it is called with the same parameters as the metaclass. So they are using a pattern (__xxx
) that is similar to the one of Python (__xxx__
).
__index and __newindex
When index operations are performed on an object this does not result in a direct call to __index
or __newindex
but in a call to __getfield
or __setfield
.
You can overwrite __getfield
and __setfield
. However, note that this breaks a lot of the usual features as depict below such as data descriptors, calling of __index
and __newindex
and redirecting reading and writing to the namespaces of the objects. But for the most purposes you should be perfectly fine with using __index
and __newindex
for access operator overloading.
Also note there is no difference between what is called __getattr__
/ __setattr__
and __getitem__
/ __setitem__
in Python.
Here is what __getfield(object, index) -> result
does exactly
- Check the namespace of the class of the object and its bases following the MRO. If there is something and it is a data descriptor (has a
__get
field), call it withobject
and return it. Otherwise move to 2. - If the object is an class instance, just check its namespace. If it is a class, check its and the namespace of its bases following its MRO. If it finds something, return it. Otherwise move to 3.
- Check the namespace of the class of the object and its bases following the MRO (similar to 1.)
- Lookup the
__index
method in the namespace of the class of the object and its bases following its MRO. If it is a function, call it withobject, index
and return the result. Otherwise move to 5. - Return
nil
Here is what __setfield(object, index, value) -> nil
does exactly
- Check the namespace of the class of the object and its bases following the MRO. If there is something and it is a data descriptor (has a
__set
field), call it withobject, value
. Otherwise move to 2. - If the object is a class instance, just check its namespace. If it is a class, check its and the namespaces of its bases following its MRO. If it finds something, move to 3. Otherwise move to 4.
- Lookup the
__newindex
method in the namespace of the class of the object and its bases following its MRO. If it is a function, call it withobject, index, value
. The results are not returned. Otherwise move to 4. - Set the index
index
of the namespace of the objectobject
to the valuevalue
.
Binary operator overloading
When overloading binary operators such as +
, it is possible that the first passed parameter is not the object of its class or it might even not be an object at all. This also hold true for their respective Lua metamethods (e.g. 42 + object
leads to object.__add(42, object)
) and is just derived from this behavior. However, with moon.isinstance
you can discern whether the first parameter is the object:
function C.__add(par1, par2) -- C is the class
if moon.isinstance(par1, C) then
-- par1 is the object
else
-- raise an error / adopt to the situation
end
end
Metaclasses
In Moon metaclasses have a similar relationship with classes what classes have with class instances. Meaning a metaclass can define operator overloading for its class and determines how objects are initialized.
When initializing a class with moon.class
, you use the moon.Class
as metaclass except you define another one. If you write a custom metaclass, I recommend to derive it from moon.Class
since it provides all necessary fields for a metaclass and provides the default item access. However, it uses __call
already in order to create new objects.
Every class has a metaclass. The metaclass of moon.Class
is moon.Class
. This is archived by some tricks in the implementation.
Since metaclasses are involved in process, I am going to explain here how an new class instance is created. Assume C
is a class and M
its metaclass.
- Call the class
C(...)
. This results in all call toM:__call(...)
. M.__call
callsC.__new(C, ...)
. IfC
inherits fromObject
, this creates and returns the new objectobj
without using any of the other parameters.M.__call
calls the constructorC.__init(obj, ...)
- Return
obj
.
Classes are created similarly. Assume M
is the metaclass and MM
its meta-metaclass.
- Call the metaclass
M(name, bases, namespace)
. Theoretically you could feed it with any parameters but those three are required in order that it works with the default metaclassmoon.Class
similar totype
of Python. The functionmoon.class
gets the new namespace by a call toM.__prepare(M, name, bases)
by the way. The call results in a call toMM.__call(name, bases, namespace)
. MM.__call
callsM.__new(M, name, bases, namespace)
. IfM
inherits frommoon.Class
, this creates a new classcls
using the given parameters.MM.__call
calls the constructorM.__init(cls, ...)
which is by default empty though.- Return
cls
.
API Documentation
moon.class([metaclass:table,] name:string [,base1:table, ...]):table
Returns a new class with an optional metaclass and bases. If no base classes are specified, the classes inherits from moon.Object
. Thus every class hierarchy initialized by this function heirs from moon.Object
. The default metaclass is moon.Class
. name
is the name of the created class.
moon.super(class:table, object:table):table
Returns a proxy of the object object
that allows calls to overwritten methods. This is done by starting to search after the position of class
in the MRO of the class of object
.
This class does not just work with class instances but also with classes in general. In this case the MRO of its metaclass is modified. Additionally, it can also handle multiple inheritance since the MRO is linear.
Nevertheless, this class can be used for modified item access only. It does not work with other types of operator overloading.
Here is a code snippet that demonstrates how it works:
function C:method(parameter)
moon.super(C, self).method(self, parameter)
...
end
Note that method
is called without colon and the object self
is passed manually. This has do be done like this as you don't want to pass the super proxy normally.
You can also use this function with data descriptors such as properties for getting and setting data:
C.value = moon.Property(
function(self)
return super(C, self).value -- use property of base class
end,
function(self, value)
super(C, self).value = value
end
)
This function is basically nothing but a wrapper of moon.Super
.
moon.issubclass(class1:table, class2:table):boolean
Returns whether the class class1
is class class2
or derives from it.
moon.isinstance(instance:table, class:table):boolean
Returns whether the class of the object instance
is a subclass of the class class
.
moon.rawget(object:table, index:any):any
Indexes the namespace of the object object
at the index index
which bypasses operator overloading. Thus being ideal for operator overloading methods just like the rawget
function of Lua with the __index
metamethod.
If obj
is a class it checks its namespace first and checks then the namespaces of its base classes following its MRO until it succeeds. With class instances just their namespace is checked.
moon.rawset(object:table, index:any, value:any):nil
Directly puts value
at the index index
into the namespace of the object object
which bypasses any operator overloading. Thus being ideal for operator overloading methods just like the rawset
function of Lua with the __newindex
metamethod.
moon.classmethod(method:function):function
Similar to Python's classmethod
decorator. Returns a wrapper of the method method
that makes sure the first parameter, the called object, is never a class instance. If the method is called with a class instance, it converts it to its class. Thus the two following code snippets call method
the same way:
class:method(...)
object:method(...) -- converts object to class
moon.staticmethod(method:function):function
Similar to Python's staticmethod
decorator. Returns a wrapper of the method method
that eats the first parameter which is the called object. Thus you can call them from anyway (class:method(...)
, object:method(...)
work both). Instead of using this function you can also simply not use the first parameter. In this case you should name it _
.
Class documentation
Class
The default metaclass (for all classes created by moon.class
).
Class:__init():nil
Empty constructor so that it can safely be omitted in sub classes since they inherit it.
Class:__getfield(index:any):any
Provides the default reading access for the class.
Class:__setfield(index:any, value:any):nil
Provides the default writing access for the class.
Class:__call(...):table
Lets the class initialize and return a new object when being called.
Class:__new(name:string, bases:table, namespace:table):table
Creates and returns a new class with the given name name
and namespace namespace
. It also calculates the MRO and depending on the base classes bases
that have to be classes. The namespace is the structure all new methods and static attributes of the class are stored in.
classmethod:
Class:__prepare():table
Returns a new namespace, by default an empty table.
Object
The default base class (for all classes created by moon.class
).
Object:__init()
Empty constructor so that it can safely be omitted in sub classes since they inherit it.
Object:__getfield(index:any):any
Provides the default reading access for the class instance.
Object:__setfield(index:any, value:any):nil
Provides the default writing access for the class instance.
classmethod:
Object:__new():table
Merely creates and returns a new class instance without calling its constructor.
Super
The proxy object used by moon.super
. Check out its documentation if you want to know how it works.
Super:__init(class:table, object:table):nil
Initializes the proxy object of object
whereby the MRO of its class is limited to all classes that come after class
in the MRO.
Super:__getfield(index:any):any
Provides the modified reading access for the object.
Super:__setfield(index:any, value:any):nil
Provides the modified writing access for the object.
Super:__new(class:table, object:table):table
Creates the proxy object for Super.__init
.
Property
The default data descriptor provided by Moon.
Property.fget
The getter function. You can access but there is no check whether it is really a function.
Property.fset
The setter function. You can access but there is no check whether it is really a function.
Property:__init(fget:function [,fset:function]):nil
Initializes the property with the getter function fget
and the optional setter function fset
.
Property:__get(object:table):any
Call the getter with the the object of its class object
.
Property:__set(object:table, value:any):nil
Call the setter with the given value value
and the the object of its class object
.
Structure of an object
Structure of a class instance
instance = {
__class = C, -- The class of the class instance
__table = { -- The namespace of the class instance
attribute1 = xyz,
...
}
}
When not accessing one of the given field, the access operation is delegated to the namespace.
Structure of a class
Cls = {
__name = "Cls", -- The name of the class
__class = M, -- The metaclass of the class
__table = { -- The namespace of the class
staticAttribute1 = xyz,
method1 = function() ... end,
...
},
__bases = {A, ..., Z} -- The base classes of the class.
__mro = {Cls, ..., Object} -- The MRO of the class.
}
When not accessing one of the given field, the access operation is delegated to the namespace (of it or its base classes).
Final notes
Performance
Moon is rather optimized on flexibility than performance. Thus you should consider not using it in time critical code. But consider the benefits of its flexibility.
Localizing
For better readability it might be worth localizing all required classes and functions of Moon. This would also accelerate your code. This way you can also use the lowercase of moon.Property
for example.
Alternative class definitions
Classes can also be defined differently than the way I used in the documentation. You could structure them with do-blocks for example and if you have a very long class name you could create an alias for it within this block:
local VeryLongClassName = moon.class("VeryLongClassName") do
local Self = VeryLongClassName -- use a static alias
function Self:__init(...)
...
end
...
end
You can even take this a step further and define a function for the class definition:
local function class(...)
local cls = moon.class(name, ...)
_ENV[cls.__name] = cls -- make global within chunk
_ENV._Self = cls
end
class("VeryLongClassName") do
local Self = _Self -- = ENV._Self
...
end
Anonymous classes
You can create a anonymous class that can be with the base class Base
like this
local object = moon.Class(
Base.__name .. "*", -- Due to debugging, you should give it a name
{Base}, {
-- overwrite method in Base
method = function(...)
...
end;
...
})() -- directly create an object