Lua Metatables
Lua - Metatables
Section titled “Lua - Metatables”Metatables are a powerful Lua feature that allows you to change the behavior of tables (and userdata) when certain operations are performed on them. Each table can have an associated metatable. A metatable itself is just a regular Lua table containing specific keys (called metamethods) that define how the original table should behave.
Key concepts involving metatables include:
- Modifying table behavior for arithmetic, order, and concatenation operators.
- Implementing object-oriented features like inheritance (
__index). - Controlling access to table fields (
__index,__newindex). - Customizing how tables are converted to strings (
__tostring) or called like functions (__call).
Setting and Getting Metatables
Section titled “Setting and Getting Metatables”setmetatable(table, metatable): Setsmetatableas the metatable fortable. Returnstable. Ifmetatableisnil, it removes the metatable oftable.getmetatable(table): Returns the metatable oftable, ornilif it doesn’t have one or if the metatable has a__metatablefield (which hides the metatable).
local my_table = {}local my_metatable = { info = "This is a metatable" }
setmetatable(my_table, my_metatable)
local retrieved_mt = getmetatable(my_table)print(retrieved_mt.info) -- Output: This is a metatableThe __metatable Metamethod
Section titled “The __metatable Metamethod”If a metatable has a field named __metatable, its value will be returned by getmetatable instead of the actual metatable. This can be used to protect metatables from external access or modification.
local protected_table = {}local actual_metatable = { __metatable = "Access Denied! This is not the real metatable." }setmetatable(protected_table, actual_metatable)
print(getmetatable(protected_table)) -- Output: Access Denied! This is not the real metatable.The __index Metamethod
Section titled “The __index Metamethod”The __index metamethod is triggered when you try to access a key in a table that doesn’t exist (i.e., the value is nil).
It can be either a function or another table:
- If
__indexis a function: Lua calls this function with the original table and the missing key as arguments:func(table, key). The function’s return value is the result of the access. - If
__indexis a table: Lua will look up the key in this__indextable instead. This is commonly used for inheritance.
Example using __index as a function (default values):
local defaults = { x = 0, y = 0, name = "Default" }local mt_index_func = { __index = function(table, key) print("Accessing missing key '" .. key .. "', returning default.") return defaults[key] -- Return from a defaults table end}
local my_obj = { x = 10, z = 100 } -- y and name are missingsetmetatable(my_obj, mt_index_func)
print(my_obj.x) -- Output: 10 (exists in my_obj)print(my_obj.y) -- Triggers __index: Output: Accessing missing key 'y', returning default. followed by 0print(my_obj.name) -- Triggers __index: Output: Accessing missing key 'name', returning default. followed by Defaultprint(my_obj.z) -- Output: 100 (exists in my_obj)print(my_obj.missing) -- Triggers __index: Output: Accessing missing key 'missing', returning default. followed by nil (as 'missing' not in defaults)Example using __index as a table (simple inheritance):
local Parent = { greeting = "Hello from Parent" }Parent.speak = function(self) print(self.greeting)end
local mt_index_table = { __index = Parent } -- If key not in Child, look in Parent
local Child = { specific_greeting = "Hi!" }setmetatable(Child, mt_index_table)
print(Child.greeting) -- Output: Hello from Parent (found via __index to Parent)Child:speak() -- Output: Hello from Parent (method found via __index)print(Child.specific_greeting) -- Output: Hi! (found directly in Child)The __newindex Metamethod
Section titled “The __newindex Metamethod”The __newindex metamethod is triggered when you try to assign a value to a key in a table that doesn’t already exist (i.e., the key is nil in the table before assignment).
It can be either a function or another table:
- If
__newindexis a function: Lua calls this function with the original table, the key, and the value being assigned:func(table, key, value). The function is responsible for storing the value, perhaps with validation or redirection. - If
__newindexis a table: Lua will perform the assignment in this__newindextable instead of the original table.
Example using __newindex as a function (read-only table or logging):
local data_store = {}local mt_newindex_func = { __newindex = function(table, key, value) print("Attempting to set '" .. key .. "' to '" .. tostring(value) .. "'") -- To make the table read-only for new keys: -- error("Cannot add new keys to this table.", 2) -- Or to log and store in a separate table: print("Storing '" .. key .. "' in separate data_store.") data_store[key] = value end, __index = function(table, key) -- To retrieve from data_store if logged return data_store[key] end}
local my_config = { host = "localhost" }setmetatable(my_config, mt_newindex_func)
my_config.host = "127.0.0.1" -- Updates existing key, __newindex not calledprint("Host:", my_config.host) -- Output: Host: 127.0.0.1
my_config.port = 8080 -- New key, triggers __newindex-- Output: Attempting to set 'port' to '8080'-- Storing 'port' in separate data_store.
print("Port from my_config:", my_config.port) -- Output: Port from my_config: 8080 (via __index)print("Port from data_store:", data_store.port) -- Output: Port from data_store: 8080rawset(table, key, value) and rawget(table, key)
Section titled “rawset(table, key, value) and rawget(table, key)”These functions allow you to set or get values in a table directly, bypassing the __index and __newindex metamethods. This is useful within metamethod implementations to avoid infinite recursion.
-- Example within a __newindex metamethod:local mt_raw = { __newindex = function(tbl, k, v) print("__newindex called for key: ", k) rawset(tbl, k, v) -- Use rawset to actually store the value in the original table end}local t_raw = {}setmetatable(t_raw, mt_raw)t_raw.new_val = 100 -- Triggers __newindexprint(t_raw.new_val) -- Output: 100 (after __newindex prints its message)Metamethods for Operators
Section titled “Metamethods for Operators”You can define metamethods to overload arithmetic, bitwise (Lua 5.3+), concatenation, and comparison operators for tables.
| Metamethod | Operator/Operation |
|---|---|
__add(a, b) | Addition (+) |
__sub(a, b) | Subtraction (-) |
__mul(a, b) | Multiplication (*) |
__div(a, b) | Float Division (/) |
__idiv(a, b) | Floor Division (//) (Lua 5.3+) |
__mod(a, b) | Modulo (%) |
__pow(a, b) | Exponentiation (^) |
__unm(a) | Unary minus (negation, -a) |
__concat(a, b) | Concatenation (..) |
__len(a) | Length operator (#a) |
__eq(a, b) | Equality (==). Note: if __eq is not provided, a == b is true only if a and b are the same object. For ~=, Lua uses the result of __eq. |
__lt(a, b) | Less than (<) |
__le(a, b) | Less than or equal (<=). If not provided, Lua tries not (b < a). |
__band(a,b) | Bitwise AND (&) (Lua 5.3+) |
__bor(a,b) | Bitwise OR (|) (Lua 5.3+) |
__bxor(a,b) | Bitwise XOR (~) (Lua 5.3+) |
__bnot(a) | Bitwise NOT (~a) (Lua 5.3+) |
__shl(a,b) | Bitwise Left Shift (<<) (Lua 5.3+) |
__shr(a,b) | Bitwise Right Shift (>>) (Lua 5.3+) |
Example: Vector addition using __add
local Vector = {}Vector.__index = Vector -- For methods, if any
function Vector:new(x, y) local vec = {x = x or 0, y = y or 0} setmetatable(vec, Vector) return vecend
Vector.__add = function(vec1, vec2) return Vector:new(vec1.x + vec2.x, vec1.y + vec2.y)end
Vector.__tostring = function(vec) return "Vector(" .. vec.x .. ", " .. vec.y .. ")"end
local v1 = Vector:new(10, 20)local v2 = Vector:new(5, 7)local v3 = v1 + v2 -- Triggers Vector.__add
print(v1) -- Output: Vector(10, 20)print(v2) -- Output: Vector(5, 7)print(v3) -- Output: Vector(15, 27)Other Important Metamethods
Section titled “Other Important Metamethods”__call(table, arg1, arg2, ...): Allows a table to be called like a function:myTable(arg1, arg2). The first argument to the metamethod is the table itself, followed by the arguments passed in the call.__tostring(table): Called bytostring()(and often byprint()) to get a string representation of the table. Must return a string.__gc(table): The finalizer. Called when the table is about to be garbage collected. Useful for releasing external resources. (Available for userdata by default; for tables, only if the table is garbage collectible and has this metamethod set before Lua 5.2 it was only for userdata, now tables can have it if they are collectable via weak tables or no references). Generally, for tables, this is more complex and involves C API or specific conditions for tables that are collectable.
Example with __call and __tostring:
local CallableAdder = { value_to_add = 0}
CallableAdder.__call = function(self, num_to_add) return self.value_to_add + num_to_addend
CallableAdder.__tostring = function(self) return "CallableAdder (adds " .. self.value_to_add .. " by default)"end
local my_adder = { value_to_add = 5 } -- An instancesetmetatable(my_adder, CallableAdder)
print(my_adder) -- Triggers __tostring: Output: CallableAdder (adds 5 by default)local result = my_adder(10) -- Triggers __call: my_adder acts as selfprint("Result of call:", result) -- Output: Result of call: 15 (5 + 10)Practical Application: Metatables are the foundation of object-oriented programming in Lua. They are also used for creating domain-specific languages (DSLs), proxy tables, read-only tables, and implementing custom data structures with specific behaviors.