Using metatables to make attempts to index nil return false

Posted by Bobble on Fri 28 Nov 2008 04:09 AM — 11 posts, 37,847 views.

Canada #0
Greets everyone,

I've run into a bit of a snag and I believe that metatables may hold the answer for me. However, I'm a little unsure how to go about getting what I need done.

Part of one of my functions looks something like this:


if characters_online.elf.warrior then
   Note ("Yay, there's at least one elf warrior online!")
end


The problem is, with the way I've set up my tables, if there's no elves online at all the characters_online.elf key doesn't exist and if this function is called I get an error stating that I tried to index a nil value.

What I would prefer is to have a situation where if the characters_online.elf key is nil, it returns a boolean false rather than an error.

Does anyone have any idea about how this might be achieved?
Australia Forum Administrator #1
One simple way, assuming your script is not more complex than you have shown, is to do this:



characters_online.elf = characters_online.elf or {}

if characters_online.elf.warrior then
   Note ("Yay, there's at least one elf warrior online!")
end


The extra line at the start will make the table for elf, if it isn't there, and then you can test if a warrior is on.
Canada #2
Excellent, this works well.

Thanks Nick. Your help is always appreciated.
Germany #3
You can also write:


if characters_online.elf and characters_online.elf.warrior then
   Note ("Yay, there's at least one elf warrior online!")
end


it's same as


if characters_online.elf then 
   if characters_online.elf.warrior then
       Note ("Yay, there's at least one elf warrior online!")
   end
end
Australia Forum Administrator #4
Or this:


if (characters_online.elf or {} ) .warrior then
   Note ("Yay, there's at least one elf warrior online!")
end


This is slightly less efficient as it potentially creates an empty table, Erendir's idea is better in the sense that it tests for the presence of an elf entry without creating one.
Amended on Mon 01 Dec 2008 03:27 AM by Nick Gammon
Australia Forum Administrator #5
Another approach is to use a metatable. This would be more general as it would handle dwarves, humans, etc. as well as elves.



-- in initialization ...

characters_online = {}

setmetatable (characters_online, { __index = function () return {} end } )


-- later on ...

if characters_online.elf.warrior then
   Note ("Yay, there's at least one elf warrior online!")
end


The metatable above for characters_online returns an empty table if you try to access a non-existent index. Thus, for any race at all you will get an empty table, unless the entry already exists.
Canada #6
Thanks everyone for the help on this, but I'm having a bit of a problem with metatables again.

I have a large table that lists all the various maladies I can be given in combat (it's an IRE MUD). This table lists the name of each affliction, it's specific cure, it's cure type and a bunch of other information. It looks like this:

cures = {
    { name =  "stupidity"  , cure = "goldenseal"  , type =  "herb" ,  given_by_whisper = "y"},
    { name = "anorexia"    , cure = "epidermal"   , type = "salve", given_by_whisper = "y"},
    { name = "sensitivity" , cure = "kelp"        , type = "herb" , given_by_whisper = "y"},
    { name = "asthma"      , cure = "kelp"        , type = "herb" , given_by_whisper = "n"}
}


I use this table (which is actually much larger) to create other tables that use things cure as the key with the maladies that are healed by that cure as the values.

For instance, I do something like this:
whisper_index = {}

for _, v in ipairs(cures)
  if v.given_by_whisper == "y" then
     whisper_index.type [v.type] = true
     whisper_index.cure [v.cure] = true
  end
end


Of course, doing it this way gives me an error as the "for" iteration starts, it tries to index whisper_index.type which is a nil value, because it hasn't been defined yet.

Now, a simple solution is to define
 whisper_index.names = {}


However, I'm making a lot of these indexes and I was thinking that I could use a metatable to create a situation where if a lua is asked to index a table that isn't there (such as above), it would create an empty table.

I tried this, thinking it would work, unfortunately, it didn't:
mt = {__index = function () return {} end}

whisper_index = {} 
  setmetatable(whisper_index, mt)


When the "for" chunk above goes through, all that I get is an empty whisper_index.names table.

Does anyone know where I've gone wrong here? I know there might be more efficient ways to do what I want to do, but I'm treating this as an exercise in learning how metatables and the __index function work.

Please let me know if you need further clarification or more details about what I'm trying to achieve.
Australia Forum Administrator #7
What the metatable does is return a value in place of the missing one. However it doesn't put it there. For example:


mt = {__index = function () return 5 end}

whisper_index = {} 

setmetatable(whisper_index, mt)

print (whisper_index ["nick"])  --> 5


This indeed prints 5 for the missing value. However it has not made whisper_index ["nick"] hold the value 5, which I think is what you are trying to do.

As far as I can see, the simplest way would be to make the subtables like this:


whisper_index = { type = {}, cure = {}, }


Canada #8
Thanks for clearing that up Nick.

Out of curiosity, is there a way to make the metatable do what I was trying to get it do? Ie. Give whisper_index["Nick"] the value of 5?
Australia Forum Administrator #9
Well this seemed to do the trick:


mt = { __index = function (tbl, key) 
                   local newitem = {}
                   tbl [key] = newitem
                   return newitem  
                 end
     }

whisper_index = {} 

setmetatable(whisper_index, mt)

whisper_index.blah = 42

print (whisper_index.blah)   --> 42



The __index function gets the table and key we are trying to set, so I have used that to create an empty table, assign it to the target table, and return the new, empty, table, so that it can be used first time through. Second time through the key will exist, and __index won't have to be called.


Canada #10
Once again Nick, thanks for your help. This let me do what I was aiming to do. I very much appreciate it.

Take care!