Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

SLua Objects: Class + Diagram

By way of summing up, at least until the next article, today we’ll look at a complete example of a simple SLua class, and a diagram showing where the metatable that makes it work fits in.

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.

In this example, we’ll create a small Person class that knows first and last name, middle initial, height in meters and weight in kilograms. Its methods will produce full name, name in last, first, middle initial form, and will calculate metric BMI.

Here are the tests I wrote for this class. I’ll show you today’s trivial testing framework in the next article.

function run_tests()
   test_correct = 0
   test_wrong = 0
   p1 = Person:new("Janet", "V", "Rossini", 1.78, 55)
   p2 = Person:new("Froggie", "T", "Gremlin", 1., 20)
   assert_equals(p1:name_full(), "Janet V. Rossini")
   assert_equals(p1:name_last_first(), "Rossini, Janet V.")
   assert_nearly_equal(p1:bmi(), 17.35, 0.05)
   assert_equals(p2:name_full(), "Froggie T. Gremlin")
   assert_nearly_equal(p2:bmi(), 20, 0.001)
   print(`Tests: correct {test_correct}, wrong {test_wrong}`)
end

When the tests run, the cube says:

[05:43] Example Class: Tests: correct 5, wrong 0

So that’s good. We see in the code above that we create a new Person instance with Person:new, listing all the values for the members of the object. We call the various functions on instance p1 by saying p1:name_full() and so on. Recall that using the : notation passes p1 into the method as the first parameter, named self inside the method. We defined, Person:name_full like this:

function Person:name_full()
   return `{self.first} {self.mi}. {self.last}`
end

Simple enough, just returns the interpolated string in pretty much the obvious way. The other two methods are defined similarly:

function Person:name_last_first()
   return `{self.last}, {self.first} {self.mi}.`
end

function Person:bmi()
   return self.weight_kg / self.height_m^2
end

Now for the meat of this meal, Person:new. We’ve seen a very similar example before and earlier articles have described the metatable mechanism that makes this work but we’re here to put a nice ribbon around all the ideas so far. Here’s Person: new.

local Person = {}

function Person:new(first, mi, last, height, weight)
   obj = {}
   self.__index = self
   setmetatable(obj, self)
   obj.first = first
   obj.mi = mi
   obj.last = last
   obj.height_m = height
   obj.weight_kg = weight
   return obj
end

I’ll ask you to skip over the self.__index and setmetatable for a moment, and notice that mostly what this function does is create a table with our field names (attributes, member variables, all the same idea) set to the values provided in the call to new, and then we return that table.

So that table, for Froggie, will be no more and no less than this:

{
   name="Froggie",
   mi="T",
   last="Gremlin",
   height_m = 1.0,
   weight_kg = 20
}

That object knows all of Froggie’s member variables … but it doesn’t know any of Froggie’s methods. That’s the job of these two lines:

   self.__index = self
   setmetatable(obj, self)

When we call Person:new, self is Person, the original empty table shown above. So we set that table to be

Person = {
   __index=Person
}

We vaguely remember that in a metatable, __index names the table to search for missing keys in the table that has that metatable. So if Person were a metatable, the table that uses it as a metatable will look in Person for missing key … like the missing methods name_last() and so on. And sure enough, the next line sets the metatable for our new instance, obj to self, which is Person.

Remember that we already put all our methods into Person, with function Person:bmi() and so on. So this diagram shows the present situation:

diagram showing two objects using same metatable

So now, when either of our instances is asked for, say, name_last_first, Lua finds that the instance’s own table does not include the key “name_last_first”. So there is what the insiders call an “event” and Lua looks in the instance’s metatable for __index. It finds a table of that name, namely Person, (the metatable again) and looks there to try to find “name_last_first”. It finds the key, grabs its value, the function, and calls it, passing our instance as self.

And voila, our instance understands the methods of the class! Isn’t that neat? I think it is.

I promised to show you how those tests worked. I’ll do that in the next article. Check it out!