Classes As Constructor Closures |
|
In ten years or more of Lua programming, I have experimented with numerous approaches to Object Orientation (OO) from "Programming in Lua", "Lua Programming Gems" and this wiki as well as schemes of my own, some with extensive 'C' support libraries. I have eventually standardised on the scheme outlined in this paper which is very flexible and relatively simple, can be implemented entirely in Lua and possesses some desirable OO characteristics that I've not seen in other Lua implementations.
It is commonly agreed that an OO system should provide encapsulation of multiple values (fields) and functions operating on those fields (methods). It is fairly commonly agreed that the system should provide an easy way of generating multiple objects sharing characteristics (most commonly method implementations). Classes or prototypes are two ways of implementing this.
I also want my classes to be seamless extensions of the Lua type system. In a pure OO language, there would be no distinction between types and classes, but a good compromise for Lua is for classes to be types but not vice versa. Given an unknown Lua value, we will need a function which can determine if it is of a specified class or type.
Another desirable characteristic is inter-working between classes and objects created in 'C' using the Lua API and those created in pure Lua. This allows 'C' libraries to provide classes and functions or methods defined in those libraries to create and return objects. It should ideally also be possible for 'C' code to create objects from classes defined in Lua.
The crux of the scheme I propose in this paper is that classes are implemented as Lua function closures that return a new object of the class when executed. All the resources required to create an object are stored in upvalues of the class closure or passed as parameters to the closure function. This means that classes are first-class Lua values independent of their storage.
Some characterised upvalues must be present in all classes to support type matching. This is referred to as the TID, for Type ID. It must also be possible to derive a TID value from objects generated by the classes so that a generic predicate function can match an object with its class. By the simple expedient of defining the TID of values which are not classes or objects to be the Lua type name, this scheme can be extended so all Lua values have a TID and can be type-matched using the same predicate function.
A nice consequence of this scheme is a constructor syntax for objects that represent collections. For example, assuming the definition of a class called List, we can write:
mylist = List{"ham", "eggs", "toast"}
This uses a standard Lua syntax shortcut to allow the function call braces to be omitted. Within the List function, we can detect that one parameter has been passed, a table with no metatable (call this a 'rawtable'), and in this case convert it in place into an object without having to copy the entries. I think this seems like a very natural extension of the Lua table constructor syntax. Alternative construction syntaxes can also, or alternatively, be provided using other parameter signatures.
Having been quite prescriptive in defining a class implementation, we can be much more flexible about the object implementation whilst still preserving universal type matching. In this section, I will show that the major object patterns suggested in the literature can all be accommodated within this scheme.
Firstly, I will demonstrate a class that produces metatable-based objects. In this pattern, the object is a Lua table (or possibly a userdata if implemented in 'C') with a metatable common to all objects of the same class. The methods and metamethods of the class are stored in this metatable while the fields are stored in the object table. The metatable is also the class TID.
do -- Class 'MetaBaseClass' local _TID = class.newmeta() -- Class Closure: local _C = function(p1) local o if class.istype(p1, 'rawtable') then o = p1 else o = {} end o.value = o.value or 0 setmetatable(o, _TID) return o end -- Methods: function _TID:mymethod() class.checkmethod(self, _C) print("Executing 'mymethod'") end -- Metamethods: function _TID.__add(p1, p2) class.checkmethod(p1, _C) class.checkmethod(p2, _C) local rv = _C() rv.value = p1.value + p2.value print("Executing add metamethod") return rv end -- Class Closure is a first-class value, for example, store it as a global: MetaBaseClass = _C end -- Class 'MetaBaseClass'
The function class.newmeta()
is a simple helper function that creates a new metatable and sets the index metamethod to self-refer. class.istype()
is a multi-purpose type testing predicate and class.checkmethod()
wraps this test and raises an error if the predicate is false (I will show an implementation of these functions later in this paper). Notice that routine use of class.checkmethod
also means that all methods of a class hold upvalue references to the class itself facilitating creation of return parameters of the same class.
To create objects, just call the class:
obj1 = MetaBaseClass{value = 13}
obj2 = MetaBaseClass{value = 10}
obj1:mymethod()
obj3 = obj1 + obj2
print(obj3.value, 23)
To support single inheritance, class.newmeta()
can be extended to take a parent class. This gets the parent TID (which is also its metatable) and sets it as the metatable of the new metatable (metametatable?). The parent class is also passed through unchanged as a syntax convenience:
do -- Class 'MetaChildClass' (Inherits MetaBaseClass) local _TID, _PC = class.newmeta(MetaBaseClass) -- Class Closure: local _C = function(p1) local o = _PC(p1) -- Extra initialisation for child class setmetatable(o, _TID) return o end -- Methods are inherited automatically, but may be overridden. -- To inherit metamethods add explicit delegations: _TID.__add = class.gettid(_PC).__add MetaChildClass = _C end -- Class 'MetaChildClass' obj4 = MetaChildClass{value = 2} obj4:mymethod() print((obj4 + obj1).value, 15)
The second pattern I will explore is prototype based. Classically, prototype approaches do not use the concept of 'class' at all; instead any object may act as a prototype for creating further objects. However I will use a standard class 'wrapper' around the prototype object and this will implement the table cloning code. The objects produced are Lua tables which contain the methods as well as the fields. If metamethods are required, a metatable will also be needed. To support type matching, a metatable will always be assigned even if it is empty.
do -- Class 'ProtoBaseClass' local _C, _PRT, _MTB = class.newproto() _PRT.field = "Hello from ProtoBaseClass" function _PRT:method() class.checkmethod(self, _C) print(self.field) end ProtoBaseClass = _C end -- Class 'ProtoBaseClass'
The support function class.newproto()
does most of the work in this case. It generates the class (function closure) and also returns references to the prototype table and the metatable ready to be populated. The class function is standardised, providing the functionality to clone the prototype table.
obj5 = ProtoBaseClass() obj5:method() -- Prints 'Hello from ProtoBaseClass' obj6 = ProtoBaseClass{field="Hello from obj6"} obj6:method() -- Prints 'Hello from obj6'
The standard class function takes an optional table parameter which can override (initialise) existing fields after they have been copied from the prototype.
This pattern can accommodate multiple inheritance (aggregation). The function class.newproto()
can accept any number of object (table) parameters. These tables, and their metatables are merged to create the prototype table and metatable. Merge is in parameter order, so fields in later parameters override those with the same name in earlier parameters.
do -- Class 'ProtoAggregateClass' local _C, _PRT = class.newproto(ProtoBaseClass()) _PRT.field = "Hello from ProtoAggregateClass" ProtoAggregateClass = _C end -- Class 'ProtoAggregateClass' obj7 = ProtoAggregateClass() obj7:method() -- Prints 'Hello from ProtoAggregateClass'
The final pattern I will consider is the single function pattern. This has an object implementation similar to the class implementation (indeed a class is also an object of this type). The object has just one method which forms a closure with the fields stored as upvalues. This pattern does not support inheritance or metamethods.
do -- Class 'FunctionClass' local _TID = "FunctionClass" local _TID_O = _TID local _C = function() local tid = _TID local val = 0 return function() local tid = _TID_O val = val + 1 return val end end FunctionClass = _C end -- Class 'FunctionClass' obj8 = FunctionClass() obj9 = FunctionClass() print(obj8(), 1) print(obj8(), 2) print(obj9(), 1)
In this pattern the TID is only used for type testing and is set as an arbitrary, but unique, string. Alternatively an empty table could be used. Because the implementation of the class and the object cannot be distinguished by Lua type, an extended tag convention is used. The tag is exactly "_TID" for the class, but has a suffix when used in the object. By this convention, a class is an object, but an object is not a class.
The TID can be derived from any Lua value using the following rules applied in order until a TID value is determined:
1. The TID is the non-nil value of any metamethod with the key '__tid'.
2. If the value is a table or userdata with a metatable, then the TID is the metatable.
3. If the value is a function with an upvalue with a name starting "_TID" (the tag), then that upvalue is the TID. This applies to function closures created in Lua.
4. If the value is a function with at least two unnamed upvalues, the first of which has a string value starting "_TID" (the tag), then the second upvalue is the TID. This applies to function closures created in 'C'.
5. Otherwise the Lua type of the value is the TID (which will be a string).
A class is a value with a TID determined by rule 3 or 4 with the tag being exactly "_TID". If the tag has any additional characters after "_TID" the value is an object, but not a class.
The following is a gettid
implementation in pure Lua:
do local getupvalue = require('debug').getupvalue class = class or {} class.gettid = function(v) -- Rule 1: metafield local mt = getmetatable(v) if mt and mt.__tid then return mt.__tid, "object_mf" end -- Rule 2: metatable if mt and (type(v) == 'userdata' or type(v) == 'table') then return mt, "object_mt" elseif type(v) == 'function' then local un, uv = getupvalue(v, 1) local r = "class" if un and un ~= "" then -- Rule 3: named upvalue local i = 2 while un and un:sub(1,4) ~= "_TID" do un, uv = getupvalue(v, i) i = i + 1 end if un and #un ~= 4 then r = "object_fn" end elseif un and type(uv) == 'string' and uv:sub(1,4) == "_TID" then -- Rule 4: upvalue pair with tag if #uv ~= 4 then r = "object_fn" end un, uv = getupvalue(v, 2) end if un then return uv, r end end -- Rule 5: Lua type name return type(v), "type" end end
Having obtained the TID of the type (or class) and that of the value (or object), identity is determined by the following rules:
1. If the TID values are Lua equal functions, then that function is called passing it the value and the type. The result of the test is the Boolean value of the first return.
2. If the TID of the value is a table which has an entry with a key number 1, then the test is true if the TID of the type matches any entry keyed with a consecutive integer from 1 up.
3. If the TID of the value is a metatable and that of the type a table, then the test is true if the type TID matches the metatable, the metatable of the metatable and so on until a metatable has no metatable.
4. If none of the above applies, the test is a simple Lua equality test between the two TID values.
The following Lua function implements these matching rules and also incorporates standard Lua type detection using a string in place of the type parameter, some additional useful type predicates and the ability to determine if two arbitrary Lua values (except strings) are of the same type:
class.istype = function(vl, ty) local tvl = type(vl) local tid1, isc = class.gettid(vl) if type(ty) == 'string' and ty ~= "" then if ty == "class" then return isc == 'class' elseif ty == "object" then return isc ~= 'type' elseif ty == "rawtable" then if tvl ~= "table" then return false end return getmetatable(vl) == nil elseif ty == "callable" then if tvl == "function" then return true end local m = getmetatable(vl) if not m then return false end return type(m.__call) == "function" else return tvl == ty end end local tid2 = class.gettid(ty) if tid2 == tvl then return true end if type(tid1) == 'function' and tid1 == tid2 then return not not (tid1(vl, ty)) end if type(tid1) == 'table' and tid1[1] then for i=1, #tid1 do if tid2 == tid1[i] then return true end end end if isc == 'object_mt' and type(tid2) == 'table' then repeat if tid2 == tid1 then return true end tid1 = getmetatable(tid1) until not tid1 end return tid1 == tid2 end class.checkmethod = function(vl, ty) if not class.istype(vl, ty) then error("Bad method call") end end -- Export "istype" as a global since it will be widely used: istype = class.istype
The support function for the metatable pattern is now straightforward:
class.newmeta = function(pcl) local mt = {} if pcl then if not class.istype(pcl,'class') then error("Bad Parent Class") end local pmt = class.gettid(pcl) if not class.istype(pmt, 'table') then error("Bad Parent Class") end setmetatable(mt, pmt) else setmetatable(mt, nil) end rawset(mt, "__index", mt) return mt, pcl end
The support function for the prototype pattern is more subtle. The outer function merges the parent objects into a new class prototype table and metatable. This metatable is also the TID for the class. The metatables of the parent objects are also referenced under numeric keys in the new metatable, which allows the istype
function to match objects of this class with objects of any of its parent classes, or classes enclosing these objects. The inner function returned in the class closure copies the prototype upvalue into a new object table, performs a final merge using any initialisation table, and sets the TID upvalue as the metatable for the new object.
class.newproto = function(...) local _TID, _PRT, pmt = {}, {}, nil for i, t in ipairs{...} do if type(t) ~= 'table' then error("prototype must be table") end pmt = getmetatable(t) for k, v in pairs(t) do _PRT[k] = v end if pmt then for k, v in pairs(pmt) do _TID[k] = v end _TID[#_TID + 1] = pmt end end local _C = function(init) local tid, prt, ob = _TID, _PRT, {} for k, v in pairs(prt) do ob[k] = v end if init then for k, v in pairs(init) do if ob[k] == nil then error("attempt to initialise non-existant field: " .. k) end if type(k) ~= 'string' or k:sub(1,1) == '_' then error("attempt to initialise private field: " .. k) end ob[k] = v end end setmetatable(ob, tid) return ob end return _C, _PRT, _TID end
Using the example classes developed earlier, the following type tests all print 'true':
print( istype(MetaBaseClass(), MetaBaseClass) ) print( istype(MetaChildClass(), MetaBaseClass) ) print( istype(MetaChildClass(), MetaChildClass) ) print( not istype(MetaBaseClass(), MetaChildClass) ) print( istype(MetaChildClass(), MetaBaseClass() ) ) print( not istype(MetaBaseClass(), MetaChildClass() ) ) print( istype(MetaBaseClass(), 'table') ) print( not istype(MetaBaseClass(), 'rawtable') ) print( istype({}, 'rawtable') ) print( istype(ProtoBaseClass(), ProtoBaseClass) ) print( not istype(ProtoAggregateClass(), MetaBaseClass) ) print( istype(ProtoAggregateClass(), ProtoAggregateClass) ) print( istype(ProtoAggregateClass(), ProtoBaseClass) ) print( istype(FunctionClass(), FunctionClass) ) print( istype(FunctionClass(), 'function') ) print( not istype(FunctionClass(), 'table') ) print( istype(FunctionClass(), 'callable') ) print( istype(FunctionClass(), 'object') ) print( not istype(FunctionClass(), 'class') ) print( istype(FunctionClass, 'class') ) print( istype(FunctionClass, 'object') ) print( istype(12, 'number') )
For an example of a hybrid implementation of these ideas partly in 'C' and partly in Lua, check out the files LibClass.h
; LibClass.cpp
and LibClass.lua
here:
https://github.com/JohnHind/Winsh.lua/tree/master/Winsh/Libraries
In particular, the implementation of the List class in these files starts off in 'C' and is subsequently completed in Lua!
By being prescriptive about the implementation of the concept of 'class', and by implementing a highly flexible type matching architecture, we can afford to be much looser about the implementation of objects. Different classes in the same Lua state can implement their objects in many different ways and still maintain a homogeneous type system in which the type of any value can be matched and characterised by a generic predicate function.
Implementing classes as function closures such that the function enclosed is the factory function or constructor for objects of that class brings big advantages. Notably classes defined in this way can be self contained and first class Lua values which do not depend on registry or global resources and are independent of storage. This approach also leverages the syntax shortcut provided for named function parameters to provide a syntax for collection class constructors which seems like a natural extension of Lua table constructors.
JohnHind (13 February 2014)