Learn Lua from JavaScript, part 3: Object-oriented behavior
JavaScript’s prototype-based classes compared to a Lua class pattern.
This post explains how Lua enables object-oriented behavior by implementing a small number of low-level language features. The key mechanic is the overloading of Lua’s key-lookup operator, which may be seen as similar to JavaScript’s prototypes. Since this overloading is the foundation, this post begins by covering operator overloading. After that I’ll take a look at JavaScript’s conceptual model in order to compare it to a class-like pattern in Lua, finishing up with an example of subclassing in Lua.
Operator overloading
Every Lua table can potentially have a metatable. A metatable is simply a Lua table that provides extra functionality for the original table, such as operator overloading. For example, two tables can be added together if their metatable has the special key __add
and the value for this key is a function that accepts the two tables as input. In that case, the expression table1 + table2
acts the same as the function call aMetatable.__add(table1, table2)
, where aMetatable
is the metatable of table1
and table2
.
The example below shows how we can use Lua tables to represent rational numbers that can be added together. It uses the setmetatable
function, which sets its 2nd argument as the metatable of its 1st argument, and then returns the 1st argument:
-- Lua
-- The fraction a/b will be represented by a table with
-- keys 'a' and 'b' holding the numerator and denominator.
fractionMetatable = {}
fractionMetatable.__add = function (f1, f2)
local a, b = f1.a * f2.b + f1.b * f2.a, f1.b * f2.b
-- The gcd function, defined in the previous post, returns
-- the greatest common divisor of a and b.
local d = gcd(a, b)
return setmetatable({a = a / d, b = b / d}, fractionMetatable)
end
fractionMetatable.__tostring = function (f)
return f.a .. '/' .. f.b -- The token `..` indicates string concatenation.
end
frac1 = setmetatable({a = 1, b = 2}, fractionMetatable) -- 1/2
frac2 = setmetatable({a = 1, b = 6}, fractionMetatable) -- 1/6
-- The following call implicitly calls __add and then __tostring.
print(frac1 + frac2) --> 2/3
Almost all of Lua’s operators can be overloaded by implementing a custom function stored in a metatable. The official list of overloadable operators, along with their corresponding special keys, can be found in Lua’s reference manual.Methods that overload operators in Lua are called metamethods.
Lua’s metatables may remind you of JavaScript’s prototypes, but they’re not quite the same. The properties of a JavaScript prototype can be seen from the inheriting object
, whereas in Lua, data stored in a metatable is not visible from the base table by default. This example illustrates the difference:
// JavaScript
a = {keyOfA: 'valueOfA'}
b = Object.create(a) // Now a is the prototype of b.
b.keyOfB = 'valueOfB'
console.log(b.keyOfA) // Prints out 'valueOfA'.
-- Lua
a = {keyOfA = 'valueOfA'}
b = setmetatable({}, a) -- Now a is the metatable of b.
b.keyOfB = 'valueOfB'
print(b.keyOfA) -- Prints out 'nil'; the lookup has failed.
Class-like behavior
Lua and JavaScript are both amenable to prototype-based programming, in which class interfaces are defined using the same language type as the instances of the class itself. I consider JavaScript’s class mechanics to be non-obvious, so I’ll review those first, and then dive into the analogous workings of Lua.
Classes in JavaScript
Here’s the traditional way of defining a class in JavaScript:
// JavaScript, pre-ES6
var Dog = function(sound) {
this.sound = sound;
}
Dog.prototype.sayHi = function() {
console.log(this.sound + '!');
}
Let’s see a usage example for this class, and then review how it works. The Dog
class can be instantiated with JavaScript’s new
operator:
// JavaScript
var rex = new Dog('woof');
rex.sayHi(); // Prints 'woof!'.
JavaScript’s new
operator begins by creating a new object whose prototype is the same as Dog
’s prototype; then it calls the Dog function with this new object set as the value of this
. Finally, the new object is given as the return value from the new
operator. The end result is a new object called rex
with the key/value pair { sound: 'woof' }
and the same prototype as Dog
.
As shown in the diagram below, both Dog
and rex
share the same prototype object. This is how JavaScript class instances are able to call methods assigned to their constructor’s prototype.
When rex.sayHi()
is called, the key 'sayHi'
is found to be missing on rex
itself, but the key is found to exist in rex
’s prototype; this function in the prototype is called. Because sayHi
was called from the rex
object—basically, because rex
was the prefix of the string rex.sayHi()
used to make the call—the sayHi
function body is executed with this = rex
. (If you’d like to understand JavaScript’s prototype-based object model in more detail, this Mozilla developer network page is a good place to start.)
ES6 introduced some fancy new notation for defining classes:
// JavaScript, ES6
class Dog {
constructor(sound) {
this.sound = sound;
}
sayHi() {
console.log(this.sound);
}
}
That code is semantically identical to the traditional way of defining a class that was shown earlier.
Classes in Lua
We’ve seen that JavaScript classes are based on prototypes, which are objects themselves. Lua is similar in that both a class interface and a class instance are viewed by the language as having the same type—specifically, both are table
s.
The key mechanism used to connect class instances to interfaces is the overloading of the __index
operator via metatables. This operator is used whenever an index is dereferenced on a table. For example, the line
a = myTable.myKey
is dereferencing the key myKey
on the table myTable
. If the __index
operator is overloaded and if myTable
does not directly have a myKey
key, then the __index
overloading function is called, and can effectively provide fallback values for keys. This is analogous to the way rex.sayHi()
worked in JavaScript even though rex
did not directly have a sayHi
key.
Let’s take a look at a typical class definition and usage in Lua, and then examine why it works:
-- Lua
-- Define the Dog class.
Dog = {}
function Dog:new(sound)
local newDog = {sound = sound}
self.__index = self
return setmetatable(newDog, self)
end
function Dog:sayHi()
print(self.sound .. '!')
end
-- Use the Dog class.
kepler = Dog:new('rarf')
kepler:sayHi() -- Prints 'rarf!'.
The Dog
table is a standard Lua table. In the definition of the constructor, the colon used in the syntax Dog:new(sound)
is syntactic sugar for Dog.new(self, sound)
. In other words, the colon-based function definition inserts a first parameter called self
. When the new
function is called, a colon is used again, as in Dog:new('rarf')
. Again, the use of the colon is syntactic sugar, this time for Dog.new(Dog, 'rarf')
. In other words, a colon-based function call inserts the object immediately before the colon as the first argument. The special variable name self
in Lua thus plays the role that this
plays in JavaScript. Whereas JavaScript binds this
implicitly, once you understand the syntactic sugar behind Lua’s colon notation, it expresses a more explicit form of binding values to self
.
The constructor Dog:new
performs essentially the same actions as JavaScript’s new
operator. Specifically:
- It creates a new table, internally called
newDog
, and assigns the value ofsound
to the key ‘sound
‘. - It sets
self.__index = self
. In our example,self = Dog
, andDog
will be the metatable of the new instance. This line makes sure that any failed key lookups on the new instance fall back to key lookups onDog
itself. - It sets
newDog
’s metatable toself
, which is the same asDog
, and returns the new instance.
The resulting table relationship is shown in the diagram below. Dog
is the metatable of the instance kepler
. Because the key ‘__index
‘ in Dog
has the value Dog
, this means that any keys not found in kepler will be looked for in Dog
.
When the function kepler:sayHi()
is called, the colon syntax is effectively the same as kepler.sayHi(kepler)
. There is no sayHi
key directly in the kepler
table, but kepler
has a metatable with an ‘__index
‘ key, so that is used to find the sayHi
key in Dog
. The end result is the same as the function call Dog.sayHi(kepler)
, where the parameter kepler
is assigned to the variable self
inside that function.
I’m not going to dive into the full details of class inheritance, but I’ll demonstrate that inheritance code in Lua can be simple. This example defines and uses a subclass of Dog
called BarkyDog
for which the sayHi
method is different, although the constructor and the sound
field are the same:
-- Define BarkyDog which inherits behavior from the Dog class.
BarkyDog = Dog:new()
function BarkyDog:sayHi()
print(self.sound .. ', ' .. self.sound .. '!')
end
-- Use the BarkyDog subclass.
benedict = BarkyDog:new('woof')
benedict:sayHi() -- Prints 'woof, woof!'
It may seem like magic that this works. Technically, nothing is happening here that hasn’t been explained, but it took me some thinking to fully grok this code pattern. The main tools here are the flexibility of self
in the Dog
constructor, along with Lua’s ability to chain lookups so that any key reference on benedict
first looks in benedict
itself, then in BarkyDog
, and finally in Dog
. This pattern is explained here in more detail.
Those are the fundamental mechanics of Lua classes. At first glance, it may seem more involved than its JavaScript counterpart. The design of Lua consistently employs smaller individual building blocks; for example, ES6 has 55% more keywords than Lua 5.3, despite offering a similar set of features. The result of finer-grained language design is greater flexibility and transparency into how the system is working. In fact, there are numerous ways to set up your object-oriented interfaces in Lua, and what I’ve covered in this post is simply one common approach.
Where to go from here
This post, along with the first and second posts in this series, has covered the essentials of Lua—but there’s much more to the language. Lua has a small but versatile set of standard libraries with operations on strings, tables, and access to files. Lua has a C API that supports extending the language with Lua-callable functions implemented in C, C++, or Objective-C. The C API also makes it easy to call Lua scripts from one of these C-family languages. LuaRocks is a package manager that can help you easily find and install Lua modules that provide a wide variety of functionality not built into the language itself.
If you’re interested in taking the next step toward Lua mastery, I humbly recommend my own quick-reference style post Learn Lua in 15 Minutes. If you enjoy long-form content with greater depth, I highly recommend Roberto Ierusalimschy’s Programming in Lua.