Hiding Lua metatable and only displaying attributes of an object

How do you create a Lua object that expands only its attributes, not its methods? For instance:

local obj = { attr1 = 1, attr2 = 2, print = function(...) print("obj print: ", ...) end, } 

It produces:

 > for k,v in pairs(obj) do print(k, v) end attr1 1 attr2 2 print function: 0x7ffe1240a310 

Also, is it possible to use colon syntax for OOP in Lua? I do not need inheritance, polymorphism, only encapsulation and privacy.

+4
source share
2 answers

I started with the above question, and while chasing the rabbit hole, I was surprised at the limited number of examples, the lack of examples for various metamethods (i.e. __ipairs , __pairs , __len ), and how few Lua 5.2 resources were on this issue.

Lua can do OOP, but IMO, as prescribed by OOP, is a poor service for the language and community (i.e., in such a way as to maintain polymorphism, multiple inheritance, etc.). There are very few reasons to use most of the Lua features for CCA for most problems. This does not necessarily mean that there is a fork on the road (for example, there is nothing to support polymorphism, which suggests that you need to use the colon syntax - you can decompose the literature described in the OOP method based on closure).

I appreciate that there are many ways to do OOP in Lua, but it is annoying to have different syntax for object attributes compared to object methods (e.g. obj.attr1 vs obj:getAttr() vs obj.method() vs obj:method() ), I want a single, unified API to interact internally and externally. To this end, the PiL 16.4 privacy section is a fantastic start, but this is an incomplete example, to which I hope to correct this answer.


The following code example:

  • emulates the class namespace MyObject = {} and saves the constructor of the object as MyObject.new()
  • hides all the details of the internal operations of objects, so that the user of the object only sees a clean table (see setmetatable() and __metatable )
  • uses closure to hide information (see Lua Pil 16.4 and Object Test Tests )
  • prevents object modification (see __newindex )
  • allows intercepting methods (see __index )
  • allows you to get a list of all functions and attributes (see the attribute 'key' in __index )
  • looks, acts, walks and talks like a regular Lua table (see __pairs , __len , __ipairs )
  • looks like a string when necessary (see __tostring )
  • works with Lua 5.2

Here is the code for creating a new MyObject (it can be a standalone function, it does not need to be stored in the MyObject table - there is nothing that links obj after creating it back to MyObject.new() , this is only for reference and without consent):

 MyObject = {} MyObject.new = function(name) local objectName = name -- A table of the attributes we want exposed local attrs = { attr1 = 123, } -- A table of the object methods (note the comma on "end,") local methods = { method1 = function() print("\tmethod1") end, print = function(...) print("MyObject.print(): ", ...) end, -- Support the less than desirable colon syntax printOOP = function(self, ...) print("MyObject:printOOP(): ", ...) end, } -- Another style for adding methods to the object (I prefer the former -- because it easier to copy/paste function() around) function methods.addAttr(k, v) attrs[k] = v print("\taddAttr: adding a new attr: " .. k .. "=\"" .. v .. "\"") end -- The metatable used to customize the behavior of the table returned by new() local mt = { -- Look up nonexistent keys in the attrs table. Create a special case for the 'keys' index __index = function(t, k) v = rawget(attrs, k) if v then print("INFO: Successfully found a value for key \"" .. k .. "\"") return v end -- 'keys' is a union of the methods and attrs if k == 'keys' then local ks = {} for k,v in next, attrs, nil do ks[k] = 'attr' end for k,v in next, methods, nil do ks[k] = 'func' end return ks else print("WARN: Looking up nonexistant key \"" .. k .. "\"") end end, __ipairs = function() local function iter(a, i) i = i + 1 local v = a[i] if v then return i, v end end return iter, attrs, 0 end, __len = function(t) local count = 0 for _ in pairs(attrs) do count = count + 1 end return count end, __metatable = {}, __newindex = function(t, k, v) if rawget(attrs, k) then print("INFO: Successfully set " .. k .. "=\"" .. v .. "\"") rawset(attrs, k, v) else print("ERROR: Ignoring new key/value pair " .. k .. "=\"" .. v .. "\"") end end, __pairs = function(t, k, v) return next, attrs, nil end, __tostring = function(t) return objectName .. "[" .. tostring(#t) .. "]" end, } setmetatable(methods, mt) return methods end 

And now using:

 -- Create the object local obj = MyObject.new("my object name") print("Iterating over all indexes in obj:") for k,v in pairs(obj) do print('', k, v) end print() print("obj has a visibly empty metatable because of the empty __metatable:") for k,v in pairs(getmetatable(obj)) do print('', k, v) end print() print("Accessing a valid attribute") obj.print(obj.attr1) obj.attr1 = 72 obj.print(obj.attr1) print() print("Accessing and setting unknown indexes:") print(obj.asdf) obj.qwer = 123 print(obj.qwer) print() print("Use the print and printOOP methods:") obj.print("Length: " .. #obj) obj:printOOP("Length: " .. #obj) -- Despite being a PITA, this nasty calling convention is still supported print("Iterate over all 'keys':") for k,v in pairs(obj.keys) do print('', k, v) end print() print("Number of attributes: " .. #obj) obj.addAttr("goosfraba", "Satoshi Nakamoto") print("Number of attributes: " .. #obj) print() print("Iterate over all keys a second time:") for k,v in pairs(obj.keys) do print('', k, v) end print() obj.addAttr(1, "value 1 for ipairs to iterate over") obj.addAttr(2, "value 2 for ipairs to iterate over") obj.addAttr(3, "value 3 for ipairs to iterate over") obj.print("ipairs:") for k,v in ipairs(obj) do print(k, v) end print("Number of attributes: " .. #obj) print("The object as a string:", obj) 

What creates the expected - and poorly formatted - output:

 Iterating over all indexes in obj: attr1 123 obj has a visibly empty metatable because of the empty __metatable: Accessing a valid attribute INFO: Successfully found a value for key "attr1" MyObject.print(): 123 INFO: Successfully set attr1="72" INFO: Successfully found a value for key "attr1" MyObject.print(): 72 Accessing and setting unknown indexes: WARN: Looking up nonexistant key "asdf" nil ERROR: Ignoring new key/value pair qwer="123" WARN: Looking up nonexistant key "qwer" nil Use the print and printOOP methods: MyObject.print(): Length: 1 MyObject.printOOP(): Length: 1 Iterate over all 'keys': addAttr func method1 func print func attr1 attr printOOP func Number of attributes: 1 addAttr: adding a new attr: goosfraba="Satoshi Nakamoto" Number of attributes: 2 Iterate over all keys a second time: addAttr func method1 func print func printOOP func goosfraba attr attr1 attr addAttr: adding a new attr: 1="value 1 for ipairs to iterate over" addAttr: adding a new attr: 2="value 2 for ipairs to iterate over" addAttr: adding a new attr: 3="value 3 for ipairs to iterate over" MyObject.print(): ipairs: 1 value 1 for ipairs to iterate over 2 value 2 for ipairs to iterate over 3 value 3 for ipairs to iterate over Number of attributes: 5 The object as a string: my object name[5] 

  • Using OOP + closures is very handy when embedding Lua as a facade or documenting an API.
  • Lua OOP can also be very, very clean and elegant (this is subjective, but there are no rules in this style) you always use . to access an attribute or method)
  • If an object behaves exactly like a table, VERY, VERY useful for writing a script and polling the state of a program.
  • Very useful when working in the sandbox.

This style consumes a bit more memory per object, but for most situations this is not a problem. Factoring the meta meta for reuse will address this, although the sample code above does not.

Last thought. Lua OOP is actually really nice when you miss most of the literature examples. I'm not saying that the literature is bad, by the way (it could not have been further from the truth!), But a set of examples of examples in PiL and other online resources leads to the use of colon syntax only (i.e., the first argument is all self functions instead of using closure or upvalue to save a reference to self ).

Hope this is a useful, more complete example.


Update (2013-10-08) . There is one notable drawback of the OOP closure-based style described above (I still think the style is worth the overhead, but I'm distracted): each instance must have its own closure. Although this is obvious in the above version of lua, it becomes a bit problematic when dealing with things on the C side.

Suppose we are talking about this C-side closure style from here. A common case on the C side is to create a userdata object via lua_newuserdata() and attach a metathesis to userdata via lua_setmetatable() . By the value of the face, this does not seem to be a problem until you understand that the methods in your meta require an increased value of user data.

 using FuncArray = std::vector<const ::luaL_Reg>; static const FuncArray funcs = { { "__tostring", LI_MyType__tostring }, }; int LC_MyType_newInstance(lua_State* L) { auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType))); new(userdata) MyType(); // Create the metatable lua_createtable(L, 0, funcs.size()); // |userdata|table| lua_pushvalue(L, -2); // |userdata|table|userdata| luaL_setfuncs(L, funcs.data(), 1); // |userdata|table| lua_setmetatable(L, -2); // |userdata| return 1; } int LI_MyType__tostring(lua_State* L) { // NOTE: Blindly assume that upvalue 1 is my userdata const auto n = lua_upvalueindex(1); lua_pushvalue(L, n); // |userdata| auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, -1)); lua_pushstring(L, myTypeInst->str()); // |userdata|string| return 1; // |userdata|string| } 

Note that the table created with lua_createtable() did not associate with the metastatic name in the same way as if you registered the meta with luaL_getmetatable() ? This is 100% good because these values ​​are completely inaccessible outside of the closure, but that means luaL_getmetatable() cannot be used to search for a specific userdata type. Similarly, this means that luaL_checkudata() and luaL_testudata() also out of bounds.

The bottom line indicates that upvalues ​​(e.g. userdata above) are associated with function calls (e.g. LI_MyType__tostring ) and are not related to userdata itself. At the moment, I do not know how you can associate an upvalue value with a value so that it becomes possible to share metathema between instances.


UPDATE (2013-10-14). I include a small example below that uses registered meta-furniture ( luaL_newmetatable() ) as well as lua_setuservalue() / lua_getuservalue() for userdata "attributes and methods". Also adding random comments that were a source of errors / fervor that I had to track down in the past. Also added a C ++ 11 trick to help with __index .

 namespace { using FuncArray = std::vector<const ::luaL_Reg>; static const std::string MYTYPE_INSTANCE_METAMETHODS{"goozfraba"}; // I use a UUID here static const FuncArray MyType_Instnace_Metamethods = { { "__tostring", MyType_InstanceMethod__tostring }, { "__index", MyType_InstanceMethod__index }, { nullptr, nullptr }, // reserve space for __metatable { nullptr, nullptr } // sentinel }; static const FuncArray MyType_Instnace_methods = { { "fooAttr", MyType_InstanceMethod_fooAttr }, { "barMethod", MyType_InstanceMethod_barMethod }, { nullptr, nullptr } // sentinel }; // Must be kept alpha sorted static const std::vector<const std::string> MyType_Instance___attrWhitelist = { "fooAttr", }; static int MyType_ClassMethod_newInstance(lua_State* L) { // You can also use an empty allocation as a placeholder userdata object // (eg lua_newuserdata(L, 0);) auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType))); new(userdata) MyType(); // Placement new() FTW // Use luaL_newmetatable() since all metamethods receive userdata as 1st arg if (luaL_newmetatable(L, MYTYPE_INSTANCE_METAMETHODS.c_str())) { // |userdata|metatable| luaL_setfuncs(L, MyType_Instnace_Metamethods.data(), 0); // |userdata|metatable| // Prevent examining the object: getmetatable(MyType.new()) == empty table lua_pushliteral(L, "__metatable"); // |userdata|metatable|literal| lua_createtable(L, 0, 0); // |userdata|metatable|literal|table| lua_rawset(L, -3); // |userdata|metatable| } lua_setmetatable(L, -2); // |userdata| // Create the attribute/method table and populate with one upvalue, the userdata lua_createtable(L, 0, funcs.size()); // |userdata|table| lua_pushvalue(L, -2); // |userdata|table|userdata| luaL_setfuncs(L, funcs.data(), 1); // |userdata|table| // Set an attribute that can only be accessed via object fooAttr, stored in key "fooAttribute" lua_pushliteral(L, "foo value is hidden in the attribute table"); // |userdata|table|literal| lua_setfield(L, -2, "fooAttribute"); // |userdata|table| // Make the attribute table the uservalue for the userdata lua_setuserdata(L, -2); // |userdata| return 1; } static int MyType_InstanceMethod__tostring(lua_State* L) { // Since we're using closures, we can assume userdata is the first value on the stack. // You can't make this assumption when using metatables, only closures. luaL_checkudata(L, 1, MYTYPE_INSTANCE_METAMETHODS.c_str()); // Test anyway auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, 1)); lua_pushstring(L, myTypeInst->str()); // |userdata|string| return 1; // |userdata|string| } static int MyType_InstanceMethod__index(lua_State* L) { lua_getuservalue(L, -2); // |userdata|key|attrTable| lua_pushvalue(L, -2); // |userdata|key|attrTable|key| lua_rawget(L, -2); // |userdata|key|attrTable|value| if (lua_isnil(L, -1)) { // |userdata|key|attrTable|value?| return 1; // |userdata|key|attrTable|nil| } // Call cfunctions when whitelisted, otherwise the caller has to call the // function. if (lua_type(L, -1) == LUA_TFUNCTION) { std::size_t keyLen = 0; const char* keyCp = ::lua_tolstring(L, -3, &keyLen); std::string key(keyCp, keyLen); if (std::binary_search(MyType_Instance___attrWhitelist.cbegin(), MyType_Instance___attrWhitelist.cend(), key)) { lua_call(L, 0, 1); } } return 1; } static int MyType_InstanceMethod_fooAttr(lua_State* L) { // Push the uservalue on to the stack from fooAttr closure (upvalue 1) lua_pushvalue(L, lua_upvalueindex(1)); // |userdata| lua_getuservalue(L, -1); // |userdata|attrTable| // I haven't benchmarked whether lua_pushliteral() + lua_rawget() // is faster than lua_getfield() - (two lua interpreter locks vs one lock + test for // metamethods). lua_pushliteral(L, "fooAttribute"); // |userdata|attrTable|literal| lua_rawget(L, -2); // |userdata|attrTable|value| return 1; } static int MyType_InstanceMethod_barMethod(lua_State* L) { // Push the uservalue on to the stack from barMethod closure (upvalue 1) lua_pushvalue(L, lua_upvalueindex(1)); // |userdata| lua_getuservalue(L, -1); // |userdata|attrTable| // Push a string to finish the example, not using userdata or attrTable this time lua_pushliteral(L, "bar() was called!"); // |userdata|attrTable|literal| return 1; } } // unnamed-namespace 

The side of the lua script looks something like this:

 t = MyType.new() print(typue(t)) --> "userdata" print(t.foo) --> "foo value is hidden in the attribute table" print(t.bar) --> "function: 0x7fb560c07df0" print(t.bar()) --> "bar() was called!" 
+9
source

how do you create a lua object that expands only its attributes and not its methods?

If you do not disclose methods in any way, you cannot name them, right? Judging by your example, it sounds as if you really want to use iterations through the attributes of the object without seeing the methods, which is true.

The easiest approach is to simply use the metatet, which puts the methods in a separate table:

 -- create Point class Point = {} Point.__index = Point function Point:report() print(self.x, self.y) end -- create instance of Point pt = setmetatable({x=10, y=20}, Point) -- call method pt:report() --> 10 20 -- iterate attributes for k,v in pairs(pt) do print(k,v) end --> x 10 y 20 

Can I use colon syntax for OOP in Lua?

Instead, you can use closure, but then pairs will see your methods.

 function Point(x, y) local self = { x=x, y=y} function pt.report() print(self.x, self.y) end return self end pt = Point(10,20) pt.report() --> 10 20 for k,v in pairs(pt) do print(k,v) end --> x 10 y 20 report function: 7772112 

You can fix the last problem by simply writing an iterator that shows only attributes:

 function nextattribute(t, k) local v repeat k,v = next(t, k) if type(v) ~= 'function' then return k,v end until k == nil end function attributes (t) return nextattribute, t, nil end for k,v in attributes(pt) do print(k,v) end --> x 10 y 20 

I do not need inheritance, polymorphism

You get polymorphism for free in Lua, with or without classes. If your zoo has Lion, Zebra, Giraffe, each of which can Eat() and wants to pass them to the same Feed(animal) procedure, in the statically typed OO languages ​​you will need to put Eat() in a common base class (for example, Animal ). Lua is dynamically typed, and your Feed procedure can be passed to any object in general. All that matters is that the object you pass has an Eat method.

This is sometimes called "duck typing" : if it dodges like a duck and swims like a duck, it is a duck. As for our subroutine Feed(animal) , if it is like an animal, it is an animal.

only encapsulation and privacy.

Then I think that exposing data elements when hiding methods is the opposite of what you want to do.

+2
source

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


All Articles