What does metatable field __index actually means? - lua

In Programming in Lua, I known the metatable field __index is a function that takes 2 arguments, they are:
table itself
key value
But something confusing happens when I was reading Object-Oriented Programming, Chapter 21, there is an example(like it):
local A = {
greet = function()
print 'hello world'
end
}
local B = {}
setmetatable(B, {
__index = A
})
B.greet()
I cant understand why __index = A let B becomes an object of A, it should be a funciton here, right? Just like following:
__index = function(_, key)
return A[key]
end

From the Lua 5.4 Reference Manual:
__index: The indexing access operation table[key]. This event happens when table is not a table or when key is not present in table. The
metavalue is looked up in the metatable of table. The metavalue for
this event can be either a function, a table, or any value with an
__index metavalue. If it is a function, it is called with table and key as arguments, and the result of the call (adjusted to one value)
is the result of the operation. Otherwise, the final result is the
result of indexing this metavalue with key. This indexing is regular,
not raw, and therefore can trigger another __index metavalue.
In your example __index is assigned A. So if a key is not present in B it is looked up in A.

The __index metamethod will be triggered when a key not exists.
So take a deeper look at...
tab = setmetatable({}, {__index = function(self, key) return('The Index Key ' .. key .. ' do not exists') end})
print(tab.i_am_a_human)
-- Output: The Index Key i_am_a_human do not exists
tab.i_am_a_human = 'yes'
print(tab.i_am_a_human)
-- Output: yes
I personally prefer a __index table with functions that will become methods for the table.
Lets say i wanna a table with all table library functions...
tab = setmetatable({}, {__index = table})
tab:insert('Hello')
tab:insert('World')
print(tab:concat(' '))
-- Output: Hello World

Related

attempt to call a nil value (method '...') if __index not implemented in metatable

Lua doesn't offer a unique way to OOP.
With setmetatable many alternatives are possible.
Here's what I tried:
Person={}
function Person.__call(cls,name)
return setmetatable({name=name},cls)
end
function Person:say(what)
print(self.name..'> '..what)
end
setmetatable(Person,Person)
p=Person('Fred')
p:say('hello') -- 18
which gives the error:
18: attempt to call a nil value (method 'say')
I can add:
function Person.__index(cls,k)
return Person[k]
end
and then the above code works correctly, however I do not understand why the method is not found when Person is already metatable of itself.
You need to implement the __index metavalue in order to relay indexing access operations. Having a metatable alone is not sufficient.
Also note that it is recommended to implement all meta methods befor using a table as a metatable.
Refer to the Lua 5.4 Reference Manual 2.4 Metatables and Metamethods
__index: The indexing access operation table[key]. This event happens when table is not a table or when key is not present in table. The
metavalue is looked up in the metatable of table.
The metavalue for this event can be either a function, a table, or any
value with an __index metavalue. If it is a function, it is called
with table and key as arguments, and the result of the call (adjusted
to one value) is the result of the operation. Otherwise, the final
result is the result of indexing this metavalue with key. This
indexing is regular, not raw, and therefore can trigger another
__index metavalue.
__index is that metavalue. So if you don't provide that metavalue, what should Lua do?
In the following example that metavalue is Person.
So when I call a:sayName(), Lua will find that a.sayName is nil. It will check if in a's metatable Person there is a __index metavalue. There is and in this case it's a table named Person so it will index that person with key "sayName" which results in the following function call: Person["sayName"](a)
local Person= {}
Person.__index = Person
setmetatable(Person, {
__call = function (cls, ...)
return cls:_init(...)
end,
})
function Person:_init(name)
local o= setmetatable({}, self)
o.name = name
return o
end
function Person:sayName()
print(self.name)
end
local a = Person("Lisa")
a:sayName()

Lua metamethod that fires when removing an element from a table

I am looking for a metamethod (or a workaround) that fires when removing an element from a lua table similar to the __newindex metamethod.
Ideally it would work something like the following:
local mytable = {}
local mt = {
__newindex = function(t,k,v)
rawset(t,k,v)
-- some other functionality
end,
-- This does not exist
__remove = function(t,k)
--some functionality
end
}
setmetatable(mytable,mt)
-- __newindex fires
mytable["key"] = value
-- __remove fires
mytable["key"] = nil
I have tried working with the __gc metamethod but that is not usable in this implementation due to the fact that the metamethod only triggers when the garbage collection cycle happens. I have no control over the garbage collection because the table (with the metamethods) is passed to a different script.
Possible workaround - do not store actual data within table.
Let your mytable act as a proxy, and store actual values in some shadow table. It might be allocated along with mytable, or data can be stored directly in metatable (so metatable must be created per mytable instance).
Here's example (easily broken by writing data under metamethods' name keys, but you get an idea), data will be stored within metatable:
http://ideone.com/eCOal3
local mytable = {}
local mt = {}
function mt.__newindex(t,k,new_value)
local previous_value = mt[k]
rawset(mt,k,new_value)
if previous_value and new_value == nil then
print "__remove() triggered"
end
end
mt.__index = mt
setmetatable(mytable, mt)
mytable.key = 123
print(mytable.key)
mytable.key = nil
print(mytable.key)
As assigning nil fires not metamethod at all, you will have to resort to an explicit removal function that does whatever you wanted the metamethod to do and then assign nil to the table entry.

Lua metamethod __newindex: "this event happens when table is not a table"?

The Lua 5.3 Reference Manual (in this part, scroll down) says:
__newindex: The indexing assignment table[key] = value. Like the index event, this event happens when table is not a table or when key is not present in table. The metamethod is looked up in table.
However I don't understand that the metamethod __newindex happens when table is not a table. What does that mean? I did try to re-assign a local with nil, but it didn't work (yes, I know it doesn't make sense to re-assign the table, but this would help it to be garbage-collected).
local v = {};
setmetatable(v, {
__newindex = function(t,k,v)
print("Aaahhh...!");
end
});
v = nil;
I'm using this online compiler to test it.
From the page you cited:
You can replace the metatable of tables using the setmetatable
function. You cannot change the metatable of other types from Lua code
(except by using the debug library (ยง6.10)); you should use the C API
for that.
You can't use setmetatable to change the metatable for something that's not a table, so you won't be able to verify what you expect (that the __newindex method is called when you index into something that's not a table).
Your code runs because you are setting the metatable for a table. (local v = {} creates a table.)
But reassigning the variable v to be something else means you no longer have a way to get to the table you created. If your last line were v[5] = 'Hello', then you would see that your metamethod is invoked.
EDIT
Reading your edit, it looks like you expected __newindex to get called when the table was garbage collected? I think you misunderstood the statement that __newindex is called "when table is not a table." That means if you did something like this:
local v = 5
print(v[3]) -- indexing into something that's not a table
__newindex would be called (because in the expression table[key], which here is v[3], table is not a table). But you can't actually set up __newindex via the setmetatable method, because that method only works on tables.

Removing Metatables from a table in Lua

I want to "unhook" a metatable from a table and was wondering if:
tbl = setmetatable(tbl, false) -- or nil
is the correct way to do this? I could not find any info about how to do it correctly. Do I need to use an assignment operator?
Also, would this be enough to destroy the metatable attached to the table if the metatable never had a reference and was anonymous?:
tbl = setmetatable({}, {__index = something})
-- later on:
tbl = nil
and the garbage collector would be enough to remove both tables?
According to the Lua reference, which you should always consult befor putting up a question here, setmetatable(tbl, nil) will delete the metatable of table tbl unless tbl's original metatable is protected. Or let's better say it does not delete the metatable but the reference to it. The table that served as metatable will of course not be deleted as long as there are other references to it.
Befor you ask people if a simple function call works, try it yourself.
You can use https://www.lua.org/cgi-bin/demo or any other Lua interpreter and you get your answer in seconds without involving anyone else.
Running this code:
setmetatable({}, false)
or
setmetatable({})
will result in
input:1: bad argument #2 to 'setmetatable' (nil or table expected)
Now you know that you cannot enter false and you have to enter nil explicitly.
To checkout that __metatable thing you would have read in the reference manual you could then try this code
local tbl = setmetatable({}, {__metatable = true})
setmetatable(tbl, nil)
Which results in the following output:
input:2: cannot change a protected metatable
To the second part of your question:
tbl = nil will not delte the table referenced by tbl. It will only remove the reference tbl to it.
local a = {}
local b = a
b = nil
print(a)
a is still a table. You only removed one of its references.
Once there is no reference left the garbage collector may collect the table.
setmetatable(tbl, {}) will establish a reference to the table returned by the table constructor {} and store that reference somewhere in the guts of tbl.
If tbl was the last reference to that table it will be collected as garbage at some point. Then of course the only reference to the table you set as metatable will also be gone and it will removed as well.
If you do something like that:
local a = {}
local b = setmetatable({}, a)
,
a = nil will not delete b's metatable
So yes it will remove both tables if no other reference to either one of them is left.

Lua:how to create a custom method on all tables

I'd like to create a custom contains method on Lua's table data structure that would check for the existence of a key. Usage would look something like this:
mytable = {}
table.insert(mytable, 'key1')
print(mytable.contains('key1'))
Thanks.
In Lua you cannot change ALL tables at once. You can do this with simpler types, like numbers, strings, functions, where you can modify their metatable and add a method to all strings, all functions, etc. This is already done in Lua 5.1 for strings, this is why you can do this:
local s = "<Hello world!>"
print(s:sub(2, -2)) -- Hello world!
Tables and userdata have metatables for each instance. If you want to create a table with a custom method already present, a simple table constructor will not do. However, using Lua's syntax sugar, you can do something like this:
local mytable = T{}
mytable:insert('val1')
print(mytable:findvalue('val1'))
In order to achieve this, you have to write the following prior to using T:
local table_meta = { __index = table }
function T(t)
-- returns the table passed as parameter or a new table
-- with custom metatable already set to resolve methods in `table`
return setmetatable(t or {}, table_meta)
end
function table.findvalue(tab, val)
for k,v in pairs(tab) do
-- this will return the key under which the value is stored
-- which can be used as a boolean expression to determine if
-- the value is contained in the table
if v == val then return k end
end
-- implicit return nil here, nothing is found
end
local t = T{key1='hello', key2='world'}
t:insert('foo')
t:insert('bar')
print(t:findvalue('world'), t:findvalue('bar'), t:findvalue('xxx'))
if not t:findvalue('xxx') then
print('xxx is not there!')
end
--> key2 2
--> xxx is not there!

Resources