|
| 1 | + |
| 2 | +## Overview |
| 3 | + |
| 4 | +**Metatables** provide a way to extend the functionality of ordinary tables in Lua. They allow you give tables **metamethods**. The most commonly used metamethod is the `__index` metamethod, which controls what happens after a table tries to look up a missing key. Other commonly used metamethods include `__len`, which governs what happens when you use the `#` operator on a table, and `__call`, which lets you call a table as if it were a function. Typically, a metatable will be a table of the form `table<string, fun(...): ...>`. You can set and retrive metatables by using the `setmetatable` and `getmetatable` functions as follows: |
| 5 | +```lua |
| 6 | +local tbl = {} |
| 7 | +local meta = {} |
| 8 | +setmetatable(tbl, meta) -- Set the metatable of `tbl` to be `meta` |
| 9 | + |
| 10 | +local m = getmetatable(tbl) -- retrieve the metatable of a table. Note that in this example, we have `m == meta`. |
| 11 | +``` |
| 12 | + |
| 13 | + |
| 14 | +Strictly speaking, there is no difference between a "metatable" and an "ordinary" table: any `table` can act as a metatable to any other `table`. |
| 15 | + |
| 16 | +> [!example]- Example: Any table can be a metatable. |
| 17 | +> |
| 18 | +> The following code does not produce any errors (although it doesn't do anything useful either): |
| 19 | +> ```lua |
| 20 | +> setmetatable({ a = 1, b = 2}, {c = 3, d = 4}) |
| 21 | +> ``` |
| 22 | +
|
| 23 | +You can think of a "metatable" as a way to provide a list of rules, that dictate what should happen whenever something tries to do certain things to a table. Let's continue with the above notation, where `tbl` is an "ordinary" `table` and `meta` is its metatable. Each `(key, value)` pair in `meta` takes the form `(name_of_action, response)`, where `name_of_action` is a string, and `response` is a function. Whenever your code tries to perform certain actions on `tbl`, it will check if `meta` has an entry that governs a `response` for that action. |
| 24 | +
|
| 25 | +
|
| 26 | +### In-depth Example: `__index` metamethod. |
| 27 | +
|
| 28 | +Let's work through an example to see how the various pieces fall into place. We will use the `__index` metamethod to control what a specific `table` does when we try to index a missing value from the table. We will use the following variables throughout this example. |
| 29 | +```lua |
| 30 | +local my_tbl = {a = 1, b = 2} |
| 31 | +local meta = {} |
| 32 | +setmetatable(my_tbl, meta) |
| 33 | +``` |
| 34 | +(Note that you can update metamethods _after_ calling `setmetatable`. |
| 35 | +) |
| 36 | +The `__index` metamethod should be a `function` that takes two arguments: |
| 37 | +```lua |
| 38 | +---@param self table the table being accessed. |
| 39 | +---@param key: string|int|any the key that we tried to access |
| 40 | +---@return any value |
| 41 | +function meta.__index(self, key) |
| 42 | +end |
| 43 | +``` |
| 44 | +(We haven't implemented any behavior yet, we'll do that later.) |
| 45 | + |
| 46 | +Intuitively, the `__index` metamethod works as follows: whenever you try to retrive a value from `my_tbl`, by passing a specified `key`, (i.e. by writing `my_tbl[key]`), Lua will secretly be performing the following: |
| 47 | +```lua |
| 48 | +local value = my_tbl[key] |
| 49 | +-- Tried to access a missing key! |
| 50 | +if value == nil then |
| 51 | + -- Lets see if there's a metatable. |
| 52 | + local metatbl = getmetatable(my_tbl) |
| 53 | + if metatbl ~= nil and metatbl.__index then |
| 54 | + -- Metatable exists and it has an `__index` metamethod. |
| 55 | + -- Set `value` equal to whatever the metatable says it should be. |
| 56 | + value = metatbl.__index(my_tbl, key) |
| 57 | + end |
| 58 | +end |
| 59 | +``` |
| 60 | +(The actual behavior is a bit more complicated than this, but this description is fine for the moment.) |
| 61 | + |
| 62 | +> [!warning]- `__index` is only called when the relevant key is missing from the table. |
| 63 | +> As the above example makes clear, the `__index` metamethod **will not do anything** if you try to retrieve a `key` that actually exists in a table. Why is it like this? Who knows. |
| 64 | +
|
| 65 | +Let's say that for whatever reason we want to print a message whenever something tries to access a missing value from a table. We can do that as follows: |
| 66 | +```lua |
| 67 | +function meta.__index(self, key) |
| 68 | + print(string.format("tried to access missing key from a table: %q", key)) |
| 69 | +end |
| 70 | +``` |
| 71 | +This will result in the following: |
| 72 | +```lua |
| 73 | +-- add some vaues t |
| 74 | +my_tbl.a -- "a" is present in this table, so __index isn't called. |
| 75 | +my_tbl.c -- "c" is NOT present, so the following gets printed: tried to access missing key from a table: "c" |
| 76 | +``` |
| 77 | + |
| 78 | +What if we want missing values to be treated as if they were `0`? Then we could write |
| 79 | +```lua |
| 80 | +function meta.__index(self, key) |
| 81 | + -- Someone tried to access a value that didn't exist, let's just say it was 0. |
| 82 | + return 0 |
| 83 | +end |
| 84 | +``` |
| 85 | +We now get the following: |
| 86 | +```lua |
| 87 | +my_tbl.a + my_tbl.b == 3 -- Both keys are present in the table, __index isn't called. |
| 88 | +my_tbl.a + my_tbl[10] == 1 -- the key `10` is missing, so __index gets called and it returns 0 |
| 89 | +``` |
| 90 | + |
| 91 | +What if we instead want `my_tbl` to look up missing values in some other table? |
| 92 | +Let's say we're writing some code that depends on config settings, and we have a table that stores default config settings, and another table that stores the users current config. To be precise: |
| 93 | +```lua |
| 94 | +local saved_config = {...} -- Stores user-specific settings. |
| 95 | +local default_config = {...} -- Stores default settings. |
| 96 | +``` |
| 97 | + |
| 98 | +If we added some new default settings since the user last used the mod, or if we provided a way to reset the saved config settings, then there could be keys in `default_config` that are not present in `saved_config`. It would be annoying to have check each time where a user-specific setting exists. Thankfully, we can use metatables to solve this problem: |
| 99 | +```lua |
| 100 | +local saved_config_meta = {} |
| 101 | +-- In our setting, the first parameter will always be equal to `saved_config`. |
| 102 | +-- We can ignore it because we're only interested in the contents of `default_config` |
| 103 | +function saved_config_meta.__index(_, key) |
| 104 | + -- Remember that this will only be called whenever `key` is NOT present in `saved_config`. |
| 105 | + -- So we don't need to perform any additional logic inside this function. |
| 106 | + return default_config[key] |
| 107 | +end |
| 108 | +setmetatable(saved_config, saved_config_meta) |
| 109 | +``` |
0 commit comments