I started with the above question, and while chasing the rabbit hole, I was surprised at the limited number of examples, the lack of examples for various metamethods (i.e. __ipairs , __pairs , __len ), and how few Lua 5.2 resources were on this issue.
Lua can do OOP, but IMO, as prescribed by OOP, is a poor service for the language and community (i.e., in such a way as to maintain polymorphism, multiple inheritance, etc.). There are very few reasons to use most of the Lua features for CCA for most problems. This does not necessarily mean that there is a fork on the road (for example, there is nothing to support polymorphism, which suggests that you need to use the colon syntax - you can decompose the literature described in the OOP method based on closure).
I appreciate that there are many ways to do OOP in Lua, but it is annoying to have different syntax for object attributes compared to object methods (e.g. obj.attr1 vs obj:getAttr() vs obj.method() vs obj:method() ), I want a single, unified API to interact internally and externally. To this end, the PiL 16.4 privacy section is a fantastic start, but this is an incomplete example, to which I hope to correct this answer.
The following code example:
- emulates the class namespace
MyObject = {} and saves the constructor of the object as MyObject.new() - hides all the details of the internal operations of objects, so that the user of the object only sees a clean table (see
setmetatable() and __metatable ) - uses closure to hide information (see Lua Pil 16.4 and Object Test Tests )
- prevents object modification (see
__newindex ) - allows intercepting methods (see
__index ) - allows you to get a list of all functions and attributes (see the attribute 'key' in
__index ) - looks, acts, walks and talks like a regular Lua table (see
__pairs , __len , __ipairs ) - looks like a string when necessary (see
__tostring ) - works with
Lua 5.2
Here is the code for creating a new MyObject (it can be a standalone function, it does not need to be stored in the MyObject table - there is nothing that links obj after creating it back to MyObject.new() , this is only for reference and without consent):
MyObject = {} MyObject.new = function(name) local objectName = name -- A table of the attributes we want exposed local attrs = { attr1 = 123, } -- A table of the object methods (note the comma on "end,") local methods = { method1 = function() print("\tmethod1") end, print = function(...) print("MyObject.print(): ", ...) end, -- Support the less than desirable colon syntax printOOP = function(self, ...) print("MyObject:printOOP(): ", ...) end, } -- Another style for adding methods to the object (I prefer the former -- because it easier to copy/paste function() around) function methods.addAttr(k, v) attrs[k] = v print("\taddAttr: adding a new attr: " .. k .. "=\"" .. v .. "\"") end -- The metatable used to customize the behavior of the table returned by new() local mt = { -- Look up nonexistent keys in the attrs table. Create a special case for the 'keys' index __index = function(t, k) v = rawget(attrs, k) if v then print("INFO: Successfully found a value for key \"" .. k .. "\"") return v end -- 'keys' is a union of the methods and attrs if k == 'keys' then local ks = {} for k,v in next, attrs, nil do ks[k] = 'attr' end for k,v in next, methods, nil do ks[k] = 'func' end return ks else print("WARN: Looking up nonexistant key \"" .. k .. "\"") end end, __ipairs = function() local function iter(a, i) i = i + 1 local v = a[i] if v then return i, v end end return iter, attrs, 0 end, __len = function(t) local count = 0 for _ in pairs(attrs) do count = count + 1 end return count end, __metatable = {}, __newindex = function(t, k, v) if rawget(attrs, k) then print("INFO: Successfully set " .. k .. "=\"" .. v .. "\"") rawset(attrs, k, v) else print("ERROR: Ignoring new key/value pair " .. k .. "=\"" .. v .. "\"") end end, __pairs = function(t, k, v) return next, attrs, nil end, __tostring = function(t) return objectName .. "[" .. tostring(#t) .. "]" end, } setmetatable(methods, mt) return methods end
And now using:
-- Create the object local obj = MyObject.new("my object name") print("Iterating over all indexes in obj:") for k,v in pairs(obj) do print('', k, v) end print() print("obj has a visibly empty metatable because of the empty __metatable:") for k,v in pairs(getmetatable(obj)) do print('', k, v) end print() print("Accessing a valid attribute") obj.print(obj.attr1) obj.attr1 = 72 obj.print(obj.attr1) print() print("Accessing and setting unknown indexes:") print(obj.asdf) obj.qwer = 123 print(obj.qwer) print() print("Use the print and printOOP methods:") obj.print("Length: " .. #obj) obj:printOOP("Length: " .. #obj) -- Despite being a PITA, this nasty calling convention is still supported print("Iterate over all 'keys':") for k,v in pairs(obj.keys) do print('', k, v) end print() print("Number of attributes: " .. #obj) obj.addAttr("goosfraba", "Satoshi Nakamoto") print("Number of attributes: " .. #obj) print() print("Iterate over all keys a second time:") for k,v in pairs(obj.keys) do print('', k, v) end print() obj.addAttr(1, "value 1 for ipairs to iterate over") obj.addAttr(2, "value 2 for ipairs to iterate over") obj.addAttr(3, "value 3 for ipairs to iterate over") obj.print("ipairs:") for k,v in ipairs(obj) do print(k, v) end print("Number of attributes: " .. #obj) print("The object as a string:", obj)
What creates the expected - and poorly formatted - output:
Iterating over all indexes in obj: attr1 123 obj has a visibly empty metatable because of the empty __metatable: Accessing a valid attribute INFO: Successfully found a value for key "attr1" MyObject.print(): 123 INFO: Successfully set attr1="72" INFO: Successfully found a value for key "attr1" MyObject.print(): 72 Accessing and setting unknown indexes: WARN: Looking up nonexistant key "asdf" nil ERROR: Ignoring new key/value pair qwer="123" WARN: Looking up nonexistant key "qwer" nil Use the print and printOOP methods: MyObject.print(): Length: 1 MyObject.printOOP(): Length: 1 Iterate over all 'keys': addAttr func method1 func print func attr1 attr printOOP func Number of attributes: 1 addAttr: adding a new attr: goosfraba="Satoshi Nakamoto" Number of attributes: 2 Iterate over all keys a second time: addAttr func method1 func print func printOOP func goosfraba attr attr1 attr addAttr: adding a new attr: 1="value 1 for ipairs to iterate over" addAttr: adding a new attr: 2="value 2 for ipairs to iterate over" addAttr: adding a new attr: 3="value 3 for ipairs to iterate over" MyObject.print(): ipairs: 1 value 1 for ipairs to iterate over 2 value 2 for ipairs to iterate over 3 value 3 for ipairs to iterate over Number of attributes: 5 The object as a string: my object name[5]
- Using OOP + closures is very handy when embedding Lua as a facade or documenting an API.
- Lua OOP can also be very, very clean and elegant (this is subjective, but there are no rules in this style) you always use
. to access an attribute or method) - If an object behaves exactly like a table, VERY, VERY useful for writing a script and polling the state of a program.
- Very useful when working in the sandbox.
This style consumes a bit more memory per object, but for most situations this is not a problem. Factoring the meta meta for reuse will address this, although the sample code above does not.
Last thought. Lua OOP is actually really nice when you miss most of the literature examples. I'm not saying that the literature is bad, by the way (it could not have been further from the truth!), But a set of examples of examples in PiL and other online resources leads to the use of colon syntax only (i.e., the first argument is all self functions instead of using closure or upvalue to save a reference to self ).
Hope this is a useful, more complete example.
Update (2013-10-08) . There is one notable drawback of the OOP closure-based style described above (I still think the style is worth the overhead, but I'm distracted): each instance must have its own closure. Although this is obvious in the above version of lua, it becomes a bit problematic when dealing with things on the C side.
Suppose we are talking about this C-side closure style from here. A common case on the C side is to create a userdata object via lua_newuserdata() and attach a metathesis to userdata via lua_setmetatable() . By the value of the face, this does not seem to be a problem until you understand that the methods in your meta require an increased value of user data.
using FuncArray = std::vector<const ::luaL_Reg>; static const FuncArray funcs = { { "__tostring", LI_MyType__tostring }, }; int LC_MyType_newInstance(lua_State* L) { auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType))); new(userdata) MyType();
Note that the table created with lua_createtable() did not associate with the metastatic name in the same way as if you registered the meta with luaL_getmetatable() ? This is 100% good because these values ββare completely inaccessible outside of the closure, but that means luaL_getmetatable() cannot be used to search for a specific userdata type. Similarly, this means that luaL_checkudata() and luaL_testudata() also out of bounds.
The bottom line indicates that upvalues ββ(e.g. userdata above) are associated with function calls (e.g. LI_MyType__tostring ) and are not related to userdata itself. At the moment, I do not know how you can associate an upvalue value with a value so that it becomes possible to share metathema between instances.
UPDATE (2013-10-14). I include a small example below that uses registered meta-furniture ( luaL_newmetatable() ) as well as lua_setuservalue() / lua_getuservalue() for userdata "attributes and methods". Also adding random comments that were a source of errors / fervor that I had to track down in the past. Also added a C ++ 11 trick to help with __index .
namespace { using FuncArray = std::vector<const ::luaL_Reg>; static const std::string MYTYPE_INSTANCE_METAMETHODS{"goozfraba"};
The side of the lua script looks something like this:
t = MyType.new() print(typue(t)) --> "userdata" print(t.foo) --> "foo value is hidden in the attribute table" print(t.bar) --> "function: 0x7fb560c07df0" print(t.bar()) --> "bar() was called!"