Do not tread beyond here unless you want to begin to learn what goes on behind the curtain.
This article assumes a basic level of knowledge of Second Life scripting. If there’s something here that you’re missing, please get in touch with me in SL so that I can try to improve the article.
Last time we talked about putting function definitions into tables, which is kind of spooky. If that wasn’t spooky enough this next bit surely will be. We are slowly delving more deeply into how Lua objects work. Generally, day in and day out, we only need to know what they do, but inquiring minds etc.
Any Lua table can have an associated “metatable”. I say “associated” because unlike anything that’s actually in the table, you code can only see the metatable of table foo
by calling getmetable(foo)
, and set foo
’s metatable to table bar
by saying setmetatable(foo,bar)
. One question you may be asking is “Why might I do that?”, and it’s a good question indeed.
When you access something from a Lua table that isn’t there, you typically get a nil
. So given our person table from last time:
function make_person(name)
local person = {name=name, first=0, found=0}
function person:display()
print(`{self.name} found {self.found} eggs and was first {self.first} times!`)
end
return person
end
If we were to say:
person = make_person("Janet")
print(person.foo)
It would print “nil”.
But if we were to create a separate table with a foo
in it and a curious definition of __index
, and set it to be Janet’s metatable, like this:
local janet = make_person("Janet")
print(`first foo is {janet.foo}`)
local meta = {foo=17}
meta.__index = meta
setmetatable(janet, meta)
print(`but now foo is {janet.foo}`)
We see this display:
[09:38] Scores: first foo is nil
[09:38] Scores: but now foo is 17
Magic? Almost, but it is magic that we are going to like. To get to that point we need to consider the so-called “metamethod”, __index
. (That’s two underbars index.)
When Lua looks in a table for something like “foo”, if “foo” isn’t there, but the table has a metatable and the metatable has a key __index
pointing to a table, Lua will look into that table for “foo” and if it finds it, returns it just as if it had been in the original table all along. So in our example above, it’s just as if it had found “foo” in our original person, even though it actually comes from the table that __index
points to … which happens to be the metatable itself, because we said meta.__index= meta
. Index could point to any table, and metatables can be reused.
So imagine for a moment that we could create a single metatable for all our person
tables, containing our display
function and a suitable __index
entry. Then every person would still understand display
, but there would be only one copy of the function, and no extra space taken up in the individual person
entries.
So in our game scoring example, we could provide a number of functions, all inside the person tables’ metatable, perhaps functions like first
and found
, so that our listen
code wouldn’t have to know what to do other than just call those functions. We could keep all the logic, whatever it was, inside the person table.
That is the essence of object-oriented thinking: we create objects, tables in Lua, and inside them we put all the data plus all the behavior that relates to that kind of object. We call the “kind” of an object its “class”, and back in the earlier articles in this series, we did just that, winding up with a rudimentary Person class that we used to create new instances of person. Foreshadowing what we’ll do next, our little objects here might look like this:
local person_1 = Person:new("Janet")
local person_2 = Person:new("Petunia")
There is a more robust way of creating objects than the one we used earlier, and we might explore it. although the new
method above is probably good enough for many purposes. The more robust one is more flexible and allows “inheritance”, where one object can inherit and use the methods of another parent object. Inheritance is very powerful but I find that more and more frequently, there are better ways to accomplish what inheritance does. In SLua, it may be better to keep it simple.
But we’ll probably at least explore it in one of these articles. For now, I’d say not to worry about the details under the covers but just to remember that using some fairly easily learned incantations, we can create tables that are able to help us manipulate them, and that let us keep behavior near the information that the behavior deals with.
If this feels “out there” right now, that’s OK. You can take your own time with these ideas, and apply them as little or as much as you wish, early on, later, or even never. Personally, I really like working with objects, but that doesn’t make me right, it might just make me strange.
Let’s move on to a bit more detail …
Lua defines quite a few metamethods, and they are all used to define what should happen if, without them, an error or nil might occur. These special keys, if found during lookup of a function, tell Lua what to do if the object would otherwise not know what to do about that function. Let’s consider a simple one, __tostring
, by using it.
When we go to print our person table, like this:
local janet = make_person("Janet")
print(`janet is {janet}`)
The result is unhelpful:
[09:48] Scores: janet is table: 0x0000000027767a1c
But if we put a __tostring
function in the metatable, like this:
local janet = make_person("Janet")
print(`first foo is {janet.foo}`)
local meta = {}
meta.__index = meta
meta.__tostring = function(p)
return '{name='..p.name..', first='..p.first..', found='..p.found..'}'
end
setmetatable(janet, meta)
print(`janet is {janet}`)
The print says this:
[10:14] Scores: janet is {name=Janet, first=0, found=0}
And, of course, if janet had scored (haha) the score numbers would reflect that.
Let’s pause, relax, and see what we’ve discovered.
So far, I think it’s fair to say that what we’ve coded is pretty weird, with functions in tables and metatables attached to our tables. But I think it is also fair to say that when we look at what we can do, it’s rather nice:
Why might this we worth doing? Well, in the preceding article, we put a convenient display
function into our person table, suggesting that we might be able to put all the functions that relate to our person into the person itself, keeping our code better organized and quite likely more easy to use. But that scheme had the disadvantage that our person object was going to create a new copy of its functions (methods) for each new table, and that seemed wasteful.
But what we see with __index
is that we can put things in the table’s metatable! If we use the same metatable for all our person instances, we get the benefit of the functions without the extra storage and copies of the methods.
And that’s where we want to go next, all the way to classes of objects. We’re almost there! Stick with me, and ask me questions if you have them.
Until next time, safe paths!