Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lua functions use "self" in source but no metamethod allows to use them

Tags:

oop

list

lua

I've been digging into Lua's source code, both the C source from their website and the lua files from Lua on Windows. I found something odd that I can't find any information about, as to why they chose to do this.

There are some methods in the string library that allows OOP calling, by attaching the method to the string like this:

string.format(s, e1, e2, ...)
s:format(e1, e2, ...)

So I dug into the source code for the module table, and found that functions like table.remove(), also allows for the same thing.

Here's the source code from UnorderedArray.lua:

function add(self, value)
    self[#self + 1] = value
end

function remove(self, index)
    local size = #self
    if index == size then
        self[size] = nil
    elseif (index > 0) and (index < size) then
        self[index], self[size] = self[size], nil
    end
end

Which indicate that the functions should support the colon method. Lo' and behold when I copy table into my new list, the methods carry over. Here's an example using table.insert as a method:

function copy(obj, seen) -- Recursive function to copy a table with tables
  if type(obj) ~= 'table' then return obj end
  if seen and seen[obj] then return seen[obj] end

  local s = seen or {}
  local res = setmetatable({}, getmetatable(obj))
  s[obj] = res
  for k, v in pairs(obj) do res[copy(k, s)] = copy(v, s) end
  return res
end

function count(list) -- Count a list because #table doesn't work on keyindexed tables
  local sum = 0; for i,v in pairs(list) do sum = sum + 1 end; print("Length: " .. sum)
end

function pts(s) print(tostring(s)) end -- Macro function

local list = {1, 2, 3}
pts(list.insert) --> nil

pts(table["insert"]) --> function: 0xA682A8
pts(list["insert"]) --> nil

list = copy(_G.table)
pts(table["insert"]) --> function: 0xA682A8
pts(list["insert"]) --> function: 0xA682A8

count(list) --> Length: 9

list:insert(-1, "test")

count(list) --> Length: 10

Was Lua 5.1 and newer supposed to support table methods like the string library but they decided to not implement the meta method?

EDIT:

I'll explain it a little further so people understand. Strings have metamethods attached that you can use on the strings OOP style.

s = "test"
s:sub(1,1)

But tables doesn't. Even though the methods in the table's source code allow for it using "self" functions. So the following code doesn't work:

t = {1,2,3}
t:remove(#t)

The function has a self member defined in the argument (UnorderedArray.lua:25: function remove(self,index)).

You can find the metamethods of strings by using:

for i,v in pairs(getmetatable('').__index) do
  print(i, tostring(v))
end

which prints the list of all methods available for strings:

sub     function: 0xB4ABC8
upper   function: 0xB4AB08
len     function: 0xB4A110
gfind   function: 0xB4A410
rep     function: 0xB4AD88
find    function: 0xB4A370
match   function: 0xB4AE08
char    function: 0xB4A430
dump    function: 0xB4A310
gmatch  function: 0xB4A410
reverse function: 0xB4AE48
byte    function: 0xB4A170
format  function: 0xB4A0F0
gsub    function: 0xB4A130
lower   function: 0xB4AC28

If you attach the module/library table to a table like Oka showed in the example, you can use the methods that table has just the same way the string metamethods work.

The question is: Why would Lua developers allow metamethods of strings by default but tables doesn't even though table's library and it's methods allow it in the source code?

The question was answered: It would allow a developer of a module or program to alter the metatables of all tables in the program, leading to the result where a table would behave differently from vanilla Lua when used in a program. It's different if you implement a class of a data type (say: vectors) and change the metamethods of that specific class and table, instead of changing all of Lua's standard table metamethods. This also slightly overlaps with operator overloading.

like image 778
Mossarelli Avatar asked Jan 04 '23 21:01

Mossarelli


1 Answers

If I'm understanding your question correctly, you're asking why it is not possible to do the following:

local tab = {}
tab:insert('value')

Having tables spawn with a default metatable and __index breaks some assumptions that one would have about tables.

Mainly, empty tables should be empty. If tables were to spawn with an __index metamethod lookup for the insert, sort, etc., methods, it would break the assumption that an empty table should not respond to any members.

This becomes an issue if you're using a table as a cache or memo, and you need to check if the 'insert', or 'sort' strings exist or not (think arbitrary user input). You'd need to use rawget to solve a problem that didn't need to be there in the first place.

Empty tables should also be orphans. Meaning that they should have no relations without the programmer explicitly giving them relations. Tables are the only complex data structure available in Lua, and are the foundation for a lot of programs. They need to be free and flexible. Pairing them with the the table table as a default metatable creates some inconsistencies. For example, not all tables can make use of the generic sort function - a weird cruft for dictionary-like tables.

Additionally, consider that you're utilizing a library, and that library's author has told you that a certain function returns a densely packed table (i.e., an array), so you figure that you can call :sort(...) on the returned table. What if the library author has changed the metatable of that return table? Now your code no longer works, and any generic functions built on top of a _:sort(...) paradigm can't accept these tables.


Basically put, strings and tables are two very different beasts. Strings are immutable, static, and their contents are predictable. Tables are mutable, transient, and very unpredictable.

It's much, much easier to add this in when you need it, instead of baking it into the language. A very simple function:

local meta = { __index = table }

_G.T = function (tab)
    if tab ~= nil then
        local tab_t = type(tab)

        if tab_t ~= 'table' then
            error(("`table' expected, got: `%s'"):format(tab_t), 0)
        end
    end

    return setmetatable(tab or {},  meta)
end

Now any time you want a table that responds to functions found in the table table, just prefix it with a T.

local foo = T {}

foo:insert('bar')

print(#foo) --> 1
like image 107
Oka Avatar answered Jan 13 '23 19:01

Oka