3
votes

Working through the Lua exercises in 7 More Languages in 7 Weeks and getting caught on a metatables problem.

The challenge is to overload the + operator in order to be able to concatenate tables (as though they were arrays).

{ 1, 2, 3 } + { 4, 5, 6 } -- { 1, 2, 3, 4, 5, 6 }

So I tried to use the __add metamethod and created a metatable to host it.

local mt = {
  __add = function(lhs, rhs)
    return concatenate(lhs, rhs)
  done
}

I mistakenly tried to set this metatable on the global table, but this obviously doesn't propagate down to all other tables.

In an attempt to add it to all created tables, I created a separate metatable on _G which would use the __newindex metamethod, then set the original metatable every __newindex was activated.

setmetatable(_G, {
  __newindex = function(array, index)
    setmetatable(array, mt)
  end
})

However, this didn't work:

a1 = { 1, 2, 3 }
a2 = { 4, 5, 6 }
a1 + a2

And resulted in the following error: attempt to perform arithmetic on global 'a1' (a nil value)

So I threw a print statement into the global metatable to see whether it was actually being called:

setmetatable(_G, {
  __newindex = function(array, index)
    print('new table ' .. index)
    setmetatable(array, mt)
  end
})

This only ever prints the first table to be created:

a1 = { 1, 2, 3 }
a2 = { 4, 5, 6 }
a3 = { 4, 5, 6 }
a4 = { 4, 5, 6 }

Results in:

new table a1

I expect I am accidentally overriding something, because when I remove the call to setmetatable

setmetatable(_G, {
  __newindex = function(array, index)
    print('new table ' .. index)
    --setmetatable(array, mt)
  end
})

It prints all entries as expected.

1

1 Answers

3
votes
setmetatable(_G, {
    __newindex = function(array, index)
        print('new table ' .. index)
        setmetatable(array, mt)
    end
})

When you do a1 = { 1, 2, 3 } then __newindex is going to be called with with array being _G and index being 'a1'. Then calling setmetatable(array, mt) is going to change the metatable of _G, undoing the effect of the original setmetatable call.

You probably want something more like:

setmetatable(_G, {
    __newindex = function(array, index)
        setmetatable(array[index], mt)
    end
})

But there's another problem because now the original effect of the assignment no longer takes place. __newindex is called instead, so a1 remains a nil value.

You could try array[index] = value within the __newindex function but this will call the same __newindex again. Instead, use rawset:

setmetatable(_G, {
  __newindex = function(array, index, value)
    rawset(array, index, value)
    setmetatable(array[index], mt)
  end
})

Note that __newindex takes 3 parameters.

Now, what happens when you assign a non-table to a global like b = 1?