Motivation
For project typho, I built a prototype utilizing Lua. I want to have a specific tree structure for the document where all tree elements inherit from some Node object. To implement this approach, I looked into Lua’s metatable and want to explain my approach here (tested with Lua 5.3 and 5.4).
Our goal
-
Lua has tables (called hashmap or dictionary in other languages).
-
I want to declare table P to be the parent of child table C.
-
If table C has value
v
, then returnv
forC[v]
. -
If table C does not have value
v
, then the valuev
of table P shall be returned (if it not exists, thennil
) -
One can set
C[v]
. This does not affectP
.
I called rule #4 “delegation”. Some might be reminded of class-based OOP inheritance, but this is very different. Since we don’t have classes, we don’t have a distinction of classes (blueprint) and objects (instantiation with members). As a result, we cannot inhert from blueprint to blueprint, but only delegate up a hierarchy of objects.
Our goal in code
local P = { ["common"] = true, ["unique"] = true }
local C = { ["common"] = false }
-- prints “true true”
print(P.common, P.unique)
-- prints “false nil”
-- but I want “false true”
print(C.common, C.unique)
(Remember, that C.common
equals C["common"]
in Lua)
Lua metatables
I remembered the concept of metatables wrongfully. I thought that any values are looked up in the table and if it fails, in the metatable. This is exactly the value delegation concept, I am trying to implement. But metatables have a different behavior and the documentation cannot be more explicit:
Every value in Lua can have a metatable. This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations.
So when we apply a special operation (like indexing) to a table, then the metatable is consulted directly. If you declare the special operation in the table directly, this would not change anything since they are not any object’s metatable. The indexing special operation, in particular, is not called if the requested key exists in the table.
And the set of special operations are listed underneath in the documentation. We only care about index
and newindex
:
__index
-
The indexing access operation
table[key]
. This event happens when table is not a table or when key is not present in table. The metamethod is looked up in table.Despite the name, the metamethod for this event can be either a function or a table. If it is a function, it is called with table and key as arguments, and the result of the call (adjusted to one value) is the result of the operation. If it is a table, the final result is the result of indexing this table with key. (This indexing is regular, not raw, and therefore can trigger another metamethod.)
__newindex
-
The indexing assignment
table[key] = value
. Like the index event, this event happens when table is not a table or when key is not present in table. The metamethod is looked up in table.Like with indexing, the metamethod for this event can be either a function or a table. If it is a function, it is called with table, key, and value as arguments. If it is a table, Lua does an indexing assignment to this table with the same key and value. (This assignment is regular, not raw, and therefore can trigger another metamethod.)
Whenever there is a
__newindex
metamethod, Lua does not perform the primitive assignment. (If necessary, the metamethod itself can callrawset
to do the assignment.)
Illustrating special operations
local T = { }
local M = { ["__index"] = function (self, idx)
if idx == "attr" then
return "exists"
end
end }
-- prints “nil”
print(T["attr"])
setmetatable(T, M)
-- prints “exists”
print(T["attr"])
I think you get the analogous idea for __newindex
where we receive arguments (self, idx, value)
where value
is the value to be set.
Strategy for our goal
-
We specify one object as parent where we delegate value lookups to; we call it
_parent
(this actually enables an arbitrary long chain; please don’t create cycles) -
Specifying actually requires setting the parent explicitly. So unlike before, we won’t just call
setmetatable
but invoke a function preparing the object in a desired manner (the function is calledcreating_delegating_object
).
The solution
local DelegatingMetaTable = {}
DelegatingMetaTable.__index = function (self, key)
if self._parent ~= nil then
return self._parent[key]
end
end
function create_delegating_object(object, parent_values)
object._parent = parent_values
setmetatable(object, DelegatingMetaTable)
return object
end
local p_values = { ["common"] = true, ["unique"] = true }
local c_values = { ["common"] = false }
local p = create_delegating_object(p_values, nil)
local c = create_delegating_object(c_values, p)
-- prints “true true”
print(p.common, p.unique)
-- prints “false true”
print(c.common, c.unique)
Conclusion
Lua’s metatable approach is really easy to grasp and very flexible. Our implementation shows a simple delegation mechanism.