📜 ⬆️ ⬇️

Opportunities metatablits in Lua on the example of the implementation of classes

There is no OOP in Lua. And, in general, it is not necessary: ​​convenient modularity and first-class functions are enough to implement many things. On this one could have finished, but the post is not about that. In this case, I will sign out how to work with metatables, where, as an example, step by step, the system will be implemented to work with classes in a somewhat similar python-style. For understanding, you need at least the basic basis of the language: tables, upvalues.



Vlob option


You can start with the simplest example:
local Obj = {} function Obj.spam() print 'Hello world' end --[[     : local Obj = { spam = function() print 'Hello world' end, } ]] Obj.spam() -- Hello world 

')
We got a table with one key, the value of which is a function. However, inside Obj.spam itself you cannot get a link to Obj itself (except by name due to upvalue), as long as there is no this / self, etc. inside the function.

We can "realize" it ourselves:
 local Obj = {} function Obj.spam(self) print(self) end Obj.spam(Obj) 

or provide it to lua:
 local Obj = {} function Obj.spam(self) print(self) end function Obj:spam2() print(self) end Obj:spam() --  Obj['spam'](Obj), ..    ,      ,    ,  . Obj:spam2() Obj.spam(Obj) -- table: 0x417c7d58 -- table: 0x417c7d58 -- table: 0x417c7d58 

The result of the work will be the same link, because all three self are the same.

Explicit use of a: b instead of ab (a) can be used, if desired, to visually distinguish the methods of the class Obj.foo (cls) and the methods of the instance a.foo (self).

A naive version of the constructor could look like this:
 local Obj = { var = 0, } function Obj:new(val) self:set(val or 0) return self end function Obj:set(val) self.var = val end function Obj:print() print(self.var) end local a = Obj:new(42) a:print() local b = Obj:new(100500) b:print() a:print() -- 42 -- 100500 -- 100500 


There is a reuse of the same table, which leads to the replacement of a.var inside b.set. To separate, you need to issue a new table to new:
 local Obj = { var = 0, } function Obj:set(val) self.var = val end function Obj:print() print(self.var) end function Obj:new(val) --      local inst = {} --     ,    Obj for k, v in pairs(self) do inst[k] = v end inst.new = nil --      .    :) inst:set(val or 0) return inst end local a = Obj:new(42) a:print() local b = Obj:new(100500) b:print() a:print() -- 42 -- 100500 -- 42 

It works, but it is too crooked, and you need to repeat it every time.

Metatables


In Lua, for each table (and userdata, but now it is not about them), you can set a metatable describing the behavior of this table in special cases . Such cases can be used in arithmetic (operator overloading), concatenation as strings, etc. As a small example of operator overloading and casting to a string:
 local mt = { __add = function(op1, op2) local op1 = type(op1) == 'table' and op1.val or op1 local op2 = type(op2) == 'table' and op2.val or op2 return op1 + op2 end, __tostring = function(self) return tostring(self.val) end, } local T = { val = 0, new = function(self) local inst = {} for k, v in pairs(self) do inst[k] = v end --      ,     setmetatable(inst, getmetatable(self)) return inst end, } setmetatable(T, mt) local a = T:new() a.val = 2 local b = T:new() b.val = 3 print(a) print(b) print(a + b) print(a + 10) print(100 + b) -- 2 -- 3 -- 5 -- 12 -- 103 


In this case, we are interested in the __index key used when accessing a table key that does not exist, which is used inside lua as follows:


This approach allows us to separate the description of the class from the creation of its instance:
 local T = {} local T_mt = { __index = T, --     ,       } function T.create() -- setmetatable        return setmetatable({}, T_mt) end function T:set(val) self.val = val or 0 end function T:print() print(self.val) end local a = T.create() a:set(42) local b = T.create() b:set(100500) a:print() b:print() a:print() --   a.foo = 7 print(a.foo) print(b.foo) --   T.bar = 7 print(a.bar) print(b.bar) 


The resulting a and b are empty tables that do not have the keys new, set, and print. These methods are stored in the general table T. With this approach, the a: print () call actually expands to (only the final execution branch):
 getmetatable(a).__index.print(a) 

Inside lua, this is done very quickly.

If you need to get a value only from a table, without using the magic of metatables, you can replace a.bar with rawget (a, 'bar') / rawset (a, 'bar', value).

As an additional pleasant trivia, you can implement a more familiar syntax of constructors:
 local T = {} setmetatable(T, { __call = function(cls) return cls.create() end, }) -- !   T.create()    T(): local a = T() local b = T() 


Idea development


Now you can try to put it all together in a common class generator, which will look like this:
 local OOP = {} function OOP.class(struct) --  return cls --  ,   end --          local A = OOP.class { val = 0, set = function(self, val) self.val = val or 0 end, print = function(self) print(self.val) end, } --    local a = A:create() a:print() a:set(42) a:print() 


Implementation in this volume is very simple:
 function OOP.class(struct) local struct = struct or {} local cls = {} local function _create_instance() local inst = {} for k, v in pairs(struct) do inst[k] = v end return inst end setmetatable(cls, { __index = { create = _create_instance, --  ,   }, __call = function(cls) return cls:create() --    end, }) return cls end 

Total business.

For class methods, you can save the class reference inside the table of the instance and use it later:
 -- ... local function _create_instance() local inst = {} -- ... inst.__class = cls -- ... end -- ... A.clsMeth = function(cls) print('Hello') end -- ... a.__class:clsMeth() -- a.clsMeth()   


Much more interesting is the situation with inheritance. While we analyze the unit:
 --     .     function OOP.subclass(parent) return function(struct) return OOP.class(struct, parent) end end local A = OOP.class { -- ... } local B = OOP.subclass(A) { -- B   A welcome = function(self) print('Welcome!') self:print() --      end, } local b = B() b:print() b:set(100500) b:welcome() 

For implementation, you need to make not so many edits:
 function OOP.class(struct, parent) -- 1.     local struct = struct or {} local cls = {} local function _create_instance() local base = parent and parent:create() or nil -- 2.       local inst = {} -- 3.        if base then for k, v in pairs(base) do inst[k] = v end end for k, v in pairs(struct) do inst[k] = v end inst.__class = cls return inst end setmetatable(cls, { __index = setmetatable( -- 4.     { create = _create_instance, }, { --      ,     __index = function(_, key) if parent then return parent[key] end end, } ), __call = function(cls) return cls:create() end, }) return cls end 


To create our own explicit constructors, we will describe the new method and call it when creating an instance:
 -- ... setmetatable(cls, { -- ... __call = function(cls, ...) local inst = cls:create() --    -   local new = inst.new if new then new(inst, ...) end return inst end, }) -- ... local A = OOP.class { new = function(self, text) text = text or '' print('Hello ' .. text) end, } local B = OOP.subclass(A) { } A('world') B('traveler') -- Hello world -- Hello traveler 

We did not implement an automatic call of the constructor (and indeed of any other method) of the ancestor, respectively
 local B = OOP.subclass(A) { new = function(self, text) print('B says ' .. tostring(text)) end, } B('spam') 

will not call A.new. To do this, again, you just need to make a small addition to the logic of the work, implementing the method of the instance super :)
 local B = OOP.subclass(A) { new = function(self, text) print('B says ' .. tostring(text)) self:super('from B') end, } -- ... local function super_func(self, ...) local frame = debug.getinfo(2) local mt = getmetatable(self) assert(mt and mt.__base, 'There are no super method') local func = mt.__base[frame.name] return func and func(self, ...) or nil end -- ... local function _create_instance() -- ... --    inst.super    ,       . --      /. --       pairs(struct),  .   a.super = x   . local inst = setmetatable({}, { __base = base, __index = { super = super_func, }, }) -- ... 

super is called without specifying the name of the method being called. To get it, use the debug module.
If you don't want to use it (or lua is running without it), then you can explicitly pass the name of the method.
debug.getinfo () is used to get brief information about the requested stack level: 0 - debug.getinfo itself, 1 - current (super_func), 2 - the level where super_func was called, ... We need the name of the function from which super, t was called . field name of the second level stack.
Now you can call any parent methods, not just the constructor :)

To implement private fields and methods, you can use an approach based on a naming convention as in python, or use true hiding through the scope of the module, or even through upvalues:
 local A = OOP.class((function() --      local function private(self, txt) print('Hello from ' .. txt) end return { val = 0, public = function(self) private(self, 'public') end, } end)()) 

Well, there are many options. I'm quite happy with the naming convention.

These are the capabilities provided by metatables in Lua. If you were able to read all this, then, apparently, it was not written in vain.

Full and a little more sophisticated version of the implementation can be seen here .

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


All Articles