LUA - Create Meta Table with every sub table beeing a meta table again

848 Views Asked by At

but this will get confusing for sure.

I'm still very new to LUA and one thing i haven't worked much yet with is metatables.

I need to find a way to create a meta table which runs a function on editing values. This is not a problem if i stay on "one level", so the first index. Then i can simply use the __newindex to run it. But what i'm trying to do is to run the function whenever any value is changed.

This would require some way to set any table inside the metatable to be again a metatable running the same function as the "main" metatable

In my use case that would be a "save" function:

function MySaveFunction(tbl)
   FileSave(my_settings_path, tbl)
end

MyTable = setmetatable()
MyTable.Value = value --> run MySaveFunction(MyTable.Value)

MyTable.SubTable = {} --> run setmetatable() on SubTable
MyTable.SubTable.Value = value --> run MySaveFunction(MyTable.SubTable.Value)

MyTable.SubTable.SubSubTable = {} --> run setmetatable() on SubSubTable
MyTable.SubTable.SubSubTable.Value = value --> run MySaveFunction(MyTable.SubTable.SubSubTable.Value)

MyTable.SubTable.SubSubSubTable = {} --> run setmetatable() on SubSubSubTable
MyTable.SubTable.SubSubSubTable.Value = value --> run MySaveFunction(MyTable.SubTable.SubSubSubTable.Value)

Hope someone can help me <.<

1

There are 1 best solutions below

4
On

First thing to take a note of is that __newindex and __index metamethods are only triggered when they handle nil value in target table. If you want to track every single change, you can't just use __newindex, because once you write a value, subsequent calls won't do anything. Instead, you need to use a proxy table.

Another important thing to consider is tracking paths of accessed members.

Last, but not least, you need to remember to use raw access functions like e.g. rawget when implementing your handlers. Otherwise, you may encounter stack overflows or other weird behaviour.

Let's have a trivial example to illustrate the problems:

local mt = {}
function mt.__newindex (t, key, value)
    if type(value) == "table" then
        rawset(t, key, setmetatable(value, mt)) -- Set the metatable for nested table
        -- Using `t[key] = setmetatable(value, mt)` here would cause an overflow.
    else
        print(t, key, "=", value) -- We expect to see output in stdout for each write
        rawset(t, key, value)
    end
end

local root = setmetatable({}, mt)
root.first = 1                   -- table: 0xa40c30 first   =   1
root.second = 2                  -- table: 0xa40c30 second  =   2
root.nested_table = {}           -- /nothing/
root.nested_table.another = 4    -- table: 0xa403a0 another =   4
root.first = 5                   -- /nothing/

Now, we need to deal with them. Let's start with a way to create a proxy table:

local
function make_proxy (data)
    local proxy = {}
    local metatable = {
        __index = function (_, key) return rawget(data, key) end,
        __newindex = function (_, key, value)
            if type(value) == "table" then
                rawset(data, key, make_proxy(value))
            else
                print(data, key, "=", value) -- Or your save function here!
                rawset(data, key, value)
            end
        end
    }
    return setmetatable(proxy, metatable) -- setmetatable() simply returns `proxy`
end

This way you have three tables: proxy, metatable and data. User accesses proxy, but because it's completely empty on each access either __index or __newindex metamethods from metatable are called. Those handlers access data table to retrieve or set the actual values that user is interested in.

Run this in the same way as previously and you will get an improvement:

local root = make_proxy{}
root.first = 1                   -- table: 0xa40c30 first   =   1
root.second = 2                  -- table: 0xa40c30 second  =   2
root.nested_table = {}           -- /nothing/
root.nested_table.another = 4    -- table: 0xa403a0 another =   4
root.first = 5                   -- table: 0xa40c30 first   =   5

This should give you an overview on why you should use a proxy table here and how to handle metamethods for it.

What's left is how to identify the path of the field that you are accessing. That part is covered in another answer to another question. I don't see a reason to duplicate it.