📜 ⬆️ ⬇️

Lua. Brief introduction to metatables for dummies


Writing this article led me to a large number of questions on metatables and OOP in Lua, since this is the most difficult and problematic section for students of this language, but since Lua was designed as a language for beginners and non-programmers and, in general, it has a small amount material for the development, it is unsuitable to leave it "for later", given that with the help of metatables you can work wonders and extremely elegant solutions of intricate tasks.

This publication will describe all the standard meta-methods for versions Lua 5.1-5.3, with examples. Let's start.

Metatables


What it is?

In fact, the metatable is no different from a regular table, except that it is listed as a control table.
')
It is possible to present schematically, for example, like this:



The metatable describes the reaction of the main table to influences, for example, calling the table as a function, dividing the table into an arbitrary value, or trying to extract a key from it that it does not have, thanks to special keys (Lua 5.1, if not specified otherwise):

General metamethods



Mathematical metamethods and comparison (functions)



Bit operations (functions, 5.3+ only)



Examples


Index


One of the most common metamethods, and causing the most questions.
It can be a table or a function, with arguments (self, key), where self is the table to look for, and key is the key whose value we want to get.

Based on this meta-method, a large number of features are built, such as OOP, proxy tables, default values ​​of tables, and much more.

Sometimes it can be harmful when you need to get the exact key for THIS table, in such cases they use the value = rawget (table, key) function, which is the default access function for the tables (it is called in bytecode when trying to get the value by key) .

--[[1]] foo = {} foo.key = 'value' --[[2]] mt = {__index = foo} -- setmetatable       --[[3]] bar = setmetatable({}, mt) bar.key2 = 'value2' -- : --[[4]] print('bar.key2', bar.key2) --> 'value2' --[[5]] print('bar.key', bar.key) --> 'value' --[[6]] bar.key = 'foobar' foo.foo = 'snafu' print('bar.key', bar.key) --> 'foobar' print('bar.foo', bar.foo) --> 'snafu' print('bar.foobarsnafu', bar.foobarsnafu) --> nil --[[7]] foo = {key = 'FooBar'} --    bar = setmetatable({}, {__index = foo}) snafu = setmetatable({}, {__index = bar}) print('snafu.key', snafu.key) --> 'FooBar' --[[8]] foo = {} foo.__index = foo setmetatable(foo, foo) -- print('foo.key', foo.key) --> error: loop in gettable print('foo.__index', foo.__index) --> "table: 0x12345678" print('rawget(foo, "key")', rawget(foo, "key")) --> nil --[[9]] foo = {} foo.key = 'value' setmetatable(foo, {__index = function(self, key) return key end}) print('foo.key', foo.key) --> 'value' print('foo.value', foo.value) --> 'value' print('foo.snafu', foo.snafu) --> 'snafu' --[[10]] fibs = { 1, 1 } setmetatable(fibs, { __index = function(self, v) self[v] = self[v - 1] + self[v - 2] return self[v] end }) 

What's going on here:

1. foo is a table in which we will look for keys that we don’t have.
2. mt is a table with the key __index = foo. If you attach it to something like a metatable, it will indicate: "If you have no keys, try finding them in foo."
3. Here is the process of clinging the metatable mt to an empty table (which becomes bar)
4. An example of direct access to table keys. In this case, we take the key as usual from the bar table.
5. An example of access to keys on __index. In this case, in the bar table there is no key ["key"], and we look for it by __index metatables - in the foo table.
6. Specification: if we enter the key key in the bar table, it will be found in it and if we try to pick up the value, the chain of metamethods will not be called. But all other non-existent keys, such as [“foo”], will continue to invoke the metamethod chain. The key [“foobarsnafu”] is missing in both tables, and its value is a logical nil.
7. Index allows you to create search chains. In this case, the key search algorithm is as follows:

1. We are looking for the key ["key"] in the snafu table.
2. Not found. The metatable snafu has the __index key pointing to the bar table. We are looking for there.
3. Again not found. But there is also a metatable with the __index key pointing to the table foo. Are looking for.
4. Found! Here it is our key, and its value is “FooBar”!



8. In this case, we create a table with the key __index, equal to it, and set it as a metatable for ourselves. When trying to get a value for any missing key, a recursive cycle of searching within itself, and __index transitions of the metatable and further search occurs. Therefore, it is better not to make closed search chains. If you use rawget, no metamethod is invoked, and we get the exact value of this key.
9. As a __index key, a metatable can have a function with two arguments - self - the table itself, key - the key whose value we want to get. The return value of the function becomes the value. With this, you can create arbitrary indexing on the tables, or create a proxy.
10. An example is taken from Wikipedia. In this case, the __index of the fibs table automatically recalculates the values ​​of Fibonacci numbers with memoization, i.e. print (fibs [10]) will print the tenth fibonacci number. Works through recursive calculation of missing table values. Subsequent values ​​are memosized into a table. It takes a little time to understand: if fibs [v - 1] is absent, the same set of actions as for fibs [v] is performed for it.

Newindex


Not so common metamethod, but also sometimes convenient for creating closed tables, filtering or proxying, and a few more things.

It can always be only a function, with arguments (self, key, value).

It can sometimes be harmful, so the rawset function (self, key, value) is used to force non-use of a given metamethod, which is a function for tables by default.

 --[[1]] foo = {} mt = {} function mt.__newindex(self, key, value) foo[key] = value end bar = setmetatable({a = 10}, mt) bar.key = 'value' print('bar.key', bar.key) --> nil print('foo.key', foo.key) --> 'value' --[[2]] bar.a = 20 print('bar.a', bar.a) --> 20 --[[3]] mt = {} function mt.__newindex(self, key, value) if type(value) == 'number' then --       __newindex rawset(self, key, value) end end foo = setmetatable({}, mt) foo.key = 'value' foo.key2 = 100500 print('foo.key', foo.key) --> nil print('foo.key2', foo.key2) --> 100500 

1. This is the simplest example of adding keys through a proxy table using the metamethod __newindex. All new value keys that we add to the bar table are added to foo according to the function. Self, in this case, the bar table;

2. __newindex applies only to non-existent keys;

3. An example of a filter function that allows you to add only numeric values ​​to a table. In the same way, we can check “add only numeric keys”, or create several tables in advance for row-tables, etc., and add values ​​to the appropriate (classification / balancing, etc.).

Call


This metamethod is convenient for reducing elements or calling default methods of functions with tables and for slightly more comfortable OOP, when we call table-class as a function, and we get an object.

 --[[1]] mt = {} function mt.__call(self, a, b, c, d) return a..b..c..d end foo = setmetatable({}, mt) foo.key = 'value' print(foo(10, 20, 30, '!')) --> 102030! print(foo.key) --> 'value' print(foo.bar) --> nil --[[2]] mt = {} --  -  ,      -- : a, b, c, d = ... function mt.__call(self, ...) return self.default(...) end foo = setmetatable({}, mt) function foo.mul2(a, b) return a * b end function foo.mul3(a, b, c) return a * b * c end foo.default = foo.mul2 print('foo.mul2(2, 3)', foo.mul2(2, 3)) --> 6 print('foo.default(2, 3)', foo.default(2, 3)) --> 6 print('foo.mul3(2, 3, 4)', foo.mul3(2, 3, 4)) --> 24 --    . print('foo(2, 3)', foo(2, 3)) --> 6 foo.default = foo.mul3 print('Default was changed') print('foo(2, 3, 4)', foo(2, 3, 4)) --> 24 

1. An example of using a metatable, a table can be called as a function. The table itself is passed as self, called as a function.

2. In this example, we fill the table with functions, and the metatable indicates that if it is called as a function, it will return the result of the function under the default key.

Tostring and Concat


Just casting an object to a string and concatenation.

 mt = {} function mt.__tostring(self) return '['..table.concat(self, ', ')..']' end foo = setmetatable({}, mt) foo[1] = 10 foo[2] = 20 foo[3] = 30 print('foo', foo) --> [10, 20, 30] -- print('foo..foo', foo..foo) -- !   ! function mt.__concat(a, b) return tostring(a)..tostring(b) end print('foo.."!"', foo.."!") --> [10, 20, 30]! print('"!"..foo', "!"..foo) --> ![10, 20, 30] print('foo..foo', foo..foo) --> [10, 20, 30][10, 20, 30] 

Metatable


Hiding metatables is sometimes useful.

 mt = {} mt.id = 12345 foo = setmetatable({}, mt) print(getmetatable(foo).id) --> 12345 mt.__metatable = 'No metatables here!' print(getmetatable(foo)) --> 'No metatables here!' mt.__metatable = false print(getmetatable(foo)) --> false 

Mode


Row, indicates the mode of relations of table values. If it contains the letter 'k', then keys will be declared weak; if it contains the letter 'v', the values ​​will become weak. You can use them together. The examples will use the function collectgarbage - forced collection of all garbage.

Tables in Lua are always passed by reference.

 --[[1]] mt = {__mode = 'v'} foo = setmetatable({}, mt) --[[2]] bar = {foobar = 'fu'} foo.key = bar foo.key2 = {barfoo = 'uf'} foo[1] = 100500 --[[3]] print('foo.key.foobar', foo.key.foobar) --> 'fu' print('foo.key2.barfoo', foo.key2.barfoo) --> 'uf' print('foo[1]', foo[1]) --> 100500 collectgarbage() print('foo.key.foobar', foo.key.foobar) --> 'fu' print('foo[1]', foo[1]) --> 100500 -- print('foo.key2.barfoo', foo.key2.barfoo) --> , key2  ! --[[4]] bar = nil collectgarbage() -- print('foo.key.foobar', foo.key.foobar) --> , key  ! 

1. An example of a table of weak values: if there are no references to values, except in this table, they will be deleted during the garbage collection process.

2. After the execution of this part of the code on the table
"{foobar = 'fu'}" there are two links (in the global space and in the table foo), and the table
"{barfoo = 'uf'}" - one, inside foo.

3. We see that while in the foo table there are all the values, but after garbage collection, the table key2 disappears. This is because there are no more strong links to it, only the weak ones that allow the scavenger to collect it. This does not apply to foo [1], since 100500 is not a reference type (not a table, not a function, not a userdata, etc., but a number).

4. If we remove the only strong bar reference, the {foobar = 'fu'} table will also be destroyed after garbage collection.

It works in a similar way with 'k', only with respect to the table reference keys (foo [{key = 'value'}] = true).

GC


The __gc function will be called if the table is collected by the garbage collector. Can be used as a finalizer. Functions with tables and cdata / userdata.

 mt = {} function mt.__gc(self) print('Table '..tostring(self)..' has been destroyed!') end -- lua 5.2+ foo = {} setmetatable(foo, mt) -- Lua 5.1 if _VERSION == 'Lua 5.1' then --  __gc   5.1     cdata-. --   -  ,   . --     'foo',    local t = foo -- newproxy  cdata-,   Lua 5.1. local proxy = newproxy(true) --  __gc  cdata -  __gc-  foo getmetatable(proxy).__gc = function(self) mt.__gc(t) end foo[proxy] = true end print(foo) foo = nil collectgarbage() --> 'Table 0x12345678 has been destroyed!' 

Len


The function that overrides the algorithm for computing the length of the table (Lua 5.2+).

 mt = {} function mt.__len(self) local keys = 0 for k, v in pairs(self) do keys = keys + 1 end return keys end foo = setmetatable({}, mt) foo[1] = 10 foo[2] = 20 foo.key = 'value' print('#foo', #foo) --> 3 (2  Lua 5.1) 

Pairs and Ipairs


Override standard table iterators for this table (Lua 5.2+).

 mt = {} function mt.__pairs(self) local key, value = next(self) return function() key, value = next(self, key) --  - . while key and type(key) == 'number' do key, value = next(self, key) end return key, value end end function mt.__ipairs(self) local i = 0 -- ipairs   . return function() i = i - 1 return self[i] and i, self[i] end end foo = setmetatable({}, mt) foo[1] = 10 foo[2] = 20 foo[-1] = -10 foo[-2] = -20 foo.foo = 'foobar' foo.bar = 'barfoo' -- Lua 5.1    , -- 5.2+ -    for k, v in pairs(foo) do print('pairs test', k, v) end --> foo foobar --> bar barfoo -- Lua 5.1      , -- 5.2+ -     for i, v in ipairs(foo) do print('ipairs test', i, v) end --> -1 -10 --> -2 -20 

Operator Overloading


Overloading of all operators works according to one scheme, detailed examples for each are not needed.

 ]] --[[1]] vector_mt = {} function vector_mt.__add(a, b) local v = {} vx = ax + bx vy = ay + by return setmetatable(v, vector_mt) end function vector_mt.__div(a, b) local v = {} vx = ax / bx vy = ay / by return setmetatable(v, vector_mt) end --   function vector_mt.__tostring(self) return '['..self.x..', '..self.y..']' end vec1 = setmetatable({}, vector_mt) vec1.x = 1 vec1.y = 2 vec2 = setmetatable({}, vector_mt) vec2.x = 3 vec2.y = 4 vec3 = vec1 + vec2 print('vec3', vec3) --> [4, 6] print('vec2 / vec1', vec2 / vec1) --> [3, 2] --[[2]] mt = {} function mt.__add(a, b) local insert_position = 1 if type(a) == 'table' and getmetatable(a) == mt then insert_position = #a + 1 else a, b = b, a end table.insert(a, insert_position, b) return a end --   function mt.__tostring(self) return '['..table.concat(self, ', ')..']' end foo = setmetatable({}, mt) --[[3]] foo = 3 + 4 + foo + 10 + 20 + 'a' + 'b' print('foo', foo) --> [7, 10, 20, a, b] foo = '12345' + foo print('foo', foo) --> [12345, 7, 10, 20, a, b] 

1. An example of operator overloading on tables that behave like vectors, thanks to the metatable. The order of the arguments should be monitored; each operation returns a new table, the “vector”.

2. A table in which elements can be added using the "+" operator.
The order of addition determines whether we add an element to the end or to the beginning.

3. 3 + 4 will be executed first, so the first element is “7”.
In other cases, the following will be added to the result of the previous item:
((7 + foo -> foo) + 10 -> foo) ...

What can you do with all this?


OOP


The first thing that suggests itself is an attempt to make OOP.

Let's try to write a simple function that implements some abstract "class":

 function Class() local class = {} --   . local mClass = {__index = class} -- ,   ""  . function class.instance() return setmetatable({}, mClass) end return class end --   . Rectangle = Class() function Rectangle.new(x, y, w, h) local self = Rectangle.instance() self.x = x or 0 self.y = y or 0 self.w = w or 10 self.h = h or 10 return self end function Rectangle.area(self) return self.w * self.h end --  rect = Rectangle.new(10, 20, 30, 40) print('rect.area(rect)', rect.area(rect)) --> 1200 print('rect:area()', rect:area()) --> 1200 

Here, already something similar to OOP. There is no inheritance and any cool stuff, but this is not bad.
When rect.area is called, the object table does not have an area key, so it goes to look for it through __index at the class table, finds it, and substitutes itself with the first argument.

Small deviation from metatables: an example of the second call - the first appearance in this article of a colon. The colon is the syntactic sugar of the Lua language. If you call a function in the table with a colon and not a dot, the table itself will be substituted with the first argument to this function, so this code is equivalent.

In details:

 foo = {x = 10, y = 20} function foo.bar(self, a, b) return (self.x + a) * (self.y + b) end print('foo.bar(foo, 1, 2)', foo.bar(foo, 1, 2)) --> 242 -- ,  self "" . function foo:bar(a, b) return (self.x + a) * (self.y + b) end print('foo:bar(1, 2)', foo:bar(1, 2)) --> 242 

You can try to slightly improve this option.

First, add the ability to call the class as a function with the return of the object, second, add the ability to overload the operators of the class itself, Third, inheritance.

 function Class(parent) local class = {} local mClass = {} --      . --     . class.__index = class --       __index, --  ,   ,   . mClass.__index = parent --   Super  . class.Super = parent -- ,       function mClass:__call(...) local instance = setmetatable({}, class) --    "init" if type(class.init) == 'function' then --         init return instance, instance:init(...) end --     -  . return instance end return setmetatable(class, mClass) end --  . Shape = Class() function Shape:init(x, y) --   self     . self.x = x or 0 self.y = y or 0 return '!!!' end function Shape:posArea() return self.x * self.y end --    Shape      , --     . function Shape:__tostring() return '[' .. self.x .. ', ' .. self.y ..']' end local foo, value = Shape(10, 20) print('foo, value', foo, value) --> [10, 20] !!! --  Rectangle = Class(Shape) function Rectangle:init(x, y, w, h) --   , self -   , --     Rectangle,  . --     Super. self.Super.init(self, x, y) --      -  . self.w, self.h = self:getDefault(w, h) end function Rectangle:area() return self.w * self.h end function Rectangle:getDefault(w, h) return w or 10, h or 20 end function Rectangle:__tostring() return '['..self.x..', '..self.y..', '..self.w .. ', '..self.h..']' end rect = Rectangle(10, 20, 30, 40) print('rect', rect) --> [10, 20, 30, 40] print('rect:area()' , rect:area()) --> 30 * 40 = 1200 --    print('rect:posArea()', rect:posArea()) --> 10 * 20 = 200 

Thus, in 15 lines of useful code, it is possible to implement the maximum of the truly necessary OOP in Lua.

Of course, there is something to improve and weigh something, and such work has been done in the middleclass or hump.Class libraries, but sometimes this is useful.

By the way, if you do not want to bother with the function-classes, and you just need to write one or two classes, you can use the construction from here .

Proxy tables


Here, finally, an example of a full-fledged proxy, with tracking of actions.

 function proxy() local real_table = {} local logger = {} local metatable = {} --         function metatable:__index(key) local value = rawget(real_table, key) table.insert(logger, "Get key "..tostring(key)..' is '.. tostring(value)) return value end --     ,    function metatable:__newindex(key, value) table.insert(logger, "Set key "..tostring(key)..' as '..tostring(value)) rawset(real_table, key, value) end return setmetatable({}, metatable), logger end prox, log = proxy() prox.a = 10 prox.a = 20 print('prox.a', prox.a) --> 20 print('log', '\n'..table.concat(log, '\n')) --> Set key a as 10 --> Set key a as 20 --> Get key a, is 20 

The output table, which logs its use. In this case, the proxy table is always empty, there are no keys in it, so __newindex will be called every time.

Temporary object tables


From time to time, you may need temporary objects that exist for some time, but when there is a shortage of memory, they take up space. This example will require the presence of the Luasec library (https-requests), although with the same success you can use Luasocket , only without https.

 page = {} page.https = require'ssl.https' page.cache = setmetatable({}, {__mode = 'v'}) --     page._meta = {} function page._meta:__tostring() return 'Page: ['..self.url.. ']: '.. self.status end setmetatable(page, page) function page:__call(url) return self.cache[url] or self:request(url) end function page:request(url) local page = setmetatable({}, self._meta) page.url = url page.data, page.status, page.error, page.hate = self.https.request(url) print(page.data, page.status, page.error, page.hate) self.cache[url] = page return page end -- , , . p = page('https://yandex.ru') print('p', p) --> Page: [https://yandex.ru]: 200 print('p.status', p.status) --> 200 --     , --     -   . print('page.cache[...]', page.cache['https://yandex.ru']) --> Page: [https://yandex.ru]: 200 --     ,      "p". collectgarbage() print('page.cache[...]', page.cache['https://yandex.ru']) --> Page: [https://yandex.ru]: 200 p = nil collectgarbage() --     -   ,    . print('page.cache[...]', page.cache['https://yandex.ru']) --> Nil 

Bye all


I believe that this material is enough to more or less learn metatables, if there are interesting or funny examples - write in the comments.

For those who want to ask a bunch of questions - leave a link to chat in the cart .

Source: https://habr.com/ru/post/346892/


All Articles