I’ve been playing around with the Löve2D game engine as part of a side project. It’s a 2D engine that uses Lua for its scripting. Part of trying it out meant getting familiar with Lua.
Lua is an interesting little language. Very simple in its syntax, but also quite flexible and extensible. Out the box, the language offers very little OOP capability. There’s some special syntax to automagically declare and pass around a this
(self
in Lua) reference, but other than that is definitely “roll your own”.
If you’re not familiar with Lua, here’s a quick primer. Everything is represented by a table (think dictionary/associative array). Basically you create a reference to a table. The table can contain named values and values can be functions.
x = { a = 5 }
creates a table referred to by x
which has a single member a
that has the value 5. You can get and set the value of a
like this print(x.a)
or x["a"] = 5
. It’s a lot like JavaScript in that regard.
You can also store functions in tables.
x = { sayHi = function() print("hi") end }
You can call the function like this x.sayHi()
If you want to build an “object” you can do something like this:
obj = {val = 0, incr = function(self) self.val = self.val + 1 end }
In this example, we’ve created a table that holds a value and a “method”, incr
, that acts on the object. To call incr
you must either pass in the object ref like this obj.incr(obj)
or use the special colon syntax obj:incr()
which will automatically pass in the containing table as the first parameter.
You’ll also need to know about metatables, specifically the __index metamethod, to really understand the rest of this post.
Disclaimer: I’ve been programming professionally for years, but I’ve only been messing around with Lua for about a week.
If you search for “Lua class modules” online, the top two hits are two blogs with two very different approaches to creating modules that export OO types. Hisham’s guidelines for writing Lua modules and catwell’s response to that post.
After reading both, I prefer catwell’s approach because of the flexibility around implementation hiding and ease of changing the interface that the module exports. The examples in his post had one problem that I didn’t like: the approach of setting anonymous metatables makes implementation of metamethods clunky because they are defined separately from the rest of the methods. His approach has a benefit that because the metatable is anonymous, the internal implementation is safe from tampering because the method table itself is not directly exported (you can still get it though). There’s another option to protect the method table from monkey patching using metatable hiding which is how I address the problem below.
To demonstrate my approach, I’m going to walk through a table-based implementation of a Counter class module. The explanation will be in the code comments.
-- First, I pre-declare a table that will act as both -- the method table and the metatable. I do this because -- I like to define my 'new' function before the other -- functions so it's available if I want to return new -- objects from within other functions in the module local mt = {} -- Next, define all of the module functions as local -- functions. Doing it this way instead of directly -- hanging the methods off of the class table, allows -- for more flexibility to modify the internal -- implementation separate from the exported interface. -- This is aligned with catwell's approach. -- Define the 'new' function. The member fields are -- also defined here. Notice that that I'm setting the -- metatable to mt (that's why it needed to be pre-decl'd) local new = function(val) local obj = { val = val or 0 } return setmetatable(obj, mt) end -- Define the increment function. Notice I'm not using -- colon syntax here for the reasons catwell outlined. local incr = function(self) self.val = self.val + 1 end -- Define a tostring function local tostring = function(self) return string.format("Counter is %d", self.val) end -- Now that the implementations are defined, we can add -- them to the method table. This also gives you a nice -- place to refine the interface you want to export. -- E.g. I will export 'incr' as 'increment'. mt.increment = incr -- If the class had more methods, they would be -- exported here. Because I'm also using mt for a -- metatable, I can export metamethods too. mt.__tostring = tostring -- Now that the interface has been defined, I need to -- set up the metatable. First the metatable needs to -- use itself for method lookup. mt.__index = mt -- Next, because we don't want the method table to be -- tampered with, hide the metatable. This line will -- make getmetatable(x) return an empty table instead -- of the real metatable. If we didn't do this, consumers -- could get the metatable and because it's also the method -- table, could monkey patch the implementation. mt.__metatable = {} -- That's pretty much it. I also add a 'constructor' -- to forward the arguments to the 'new' function. local ctor = function(cls, ...) return new(...) end -- Finally return a table that can be called to get a new -- object. You could also simply return a function or a -- table with a 'new' member. It's all a matter of style and -- what syntax you want your consumers to use. return setmetatable({}, { __call = ctor })
You use my new class module like this (in the Repl).
> Counter = require "counter" > c = Counter() > print(c) Counter is 0 > c:increment() > print(c) Counter is 1 > c2 = Counter(100) > print(c2) Counter is 100
Pretty straight-forward. Notice I’ve made two instances, one that that was initialized with the default and one initialized with 100. You can see that they each have their own value that can be incremented independently using the exported name ‘increment’ (as opposed to the defined function ‘incr’). Also notice that the meta method __tostring is defined and is forwarded to the internal implementation of tostring.
Now lets test how “safe” it is. First let’s try overriding ‘increment’ on one of the objects and verify it doesn’t affect the other. I’m overriding the behavior on instance ‘c’ to increment by 10.
> c.increment = function(self) self.val = self.val + 10 end > c:increment() > print(c) Counter is 11 > c2:increment() > print(c2) Counter is 101
Good. It only affects instance ‘c’. How about if we try to explicitly patch the method on the method table? You can usually get a reference to that table by getting the __index instance from the method table.
> =getmetatable(c).__index nil
Can’t do that either because the metatable is hidden (by setting the __metatable
metamethod).
To recap, I prefer the module style described by in catwell’s blog because it allows you to more formally export an interface that isn’t directly tied to the implementation. Also, it promotes simpler function design and better implementation hiding. The difference between my approach and catwell’s only really differs in the mechanism of hiding the method table. By combining the metatable and method table instead of using an anonymous metatable, it’s clearer and cleaner when defining metamethods. This is especially useful when defining metamethods like __add
and __eq
.
That said, I would not consider myself an adept Lua programmer. If I’ve overlooked something, please help me learn by leaving a comment below.