Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

SLua Objects: Functions in Tables

This morning, we going to take a step toward understanding SLua objects, by discussing the fact that a table can have functions in it, not just data items. That’s the first step along the way to objects: there will be at least one more article about this, and probably more.

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.

Let’s get going!

If you have ever used a “strided” list in LSL, or wished that you could have a list of lists, you were probably on the edge of wanting what we call “objects”. What are they? Let’s explore.

For a script to talk about, let’s imagine that we’re writing a script that scores a hunting game in which players search for “Easter eggs”. When they find an egg, they touch it. If they are the first player to touch the egg, the egg says “first”, followed by the player’s name, using ..lRegionSay. If they are not first, it says “found” followed by the name.

Our job as scorekeeper will be to keep a record, for each player who finds at least one egg, a record of the number of first finds, and regular finds, they have. We won’t be writing the whole program here, just bits of it using it as a basis for discussion.

In Linden Scripting Language, LSL, we would probably keep the scores in a strided list with name at the front and the accumulated scores second and third. We might even use declared constants to remember where things were, because we suspect that the game creators will come up with another good idea just about as soon as we’re finished. So we might have something like this:

integer STRIDE = 3;
integer FIELD_NAME = 0;
integer FIELD_FIRST = 1;
integer FIELD_FOUND = 2;

And there would be weeping and gnashing of teeth as we search the list by stride and pull out existing scores, add to them, and stuff them back in the list.

It’s never quite that bad in SLua.

As you probably know, tables are the main (and only) data structure in SLua. A table representing a person’s score in the style of LSL might look like this:

local some_score = {"Janet", 3, 5}

In LSL, that’s close to the best we could do, and we’d just have to remember that index 1 is UUID and so on. In SLua we can do better, perhaps like this:

local some_score = {name="Janet", first=3, found=5}

Now, in SLua, if we want to report on a person and their current gold holdings, we can just say:

ll.Say(0, `{person.name} found {person.found} and was first {person.first} times!`)

Much nicer. But there’s more. A table can contain named values, as above, but it can also contain named functions.

“Wonderful”, you say, “but why would I ever want that?” Good question. Let’s imagine what the listen code might look like for our scoring program. We would be creating lots of person tables, so we might create a little function to do that:

function make_person(name)
   local person = {name=name, first=0, found=0}
   return person
end

And we might have a function to display a person’s results:

function display_person(person)
   print(`{person.name} found {person.found} eggs and was first {person.first} times!`)
end
Note
I’m using print here so as not to disturb the people around me as I build this. Our real scoring program would use ll.Say or something even more clever, no doubt. We’re here to get the essential ideas around object, so we’ll not worry about those details.

And we might have a dictionary table, by name, where we would keep all the people who had turned up, and we might have a function to add a person and one to print them all, giving us a quick prototype like this:

function make_person(name)
   local person = {name=name, first=0, found=0}
   return person
end

function display_person(person)
   print(`{person.name} found {person.found} eggs and was first {person.first} times!`)
end

local people = {}

function add_person(person)
   people[person.name] = person
end

function print_people()
   for name, person in pairs(people) do
      display_person(person)
   end
end

function state_entry()
   janet = make_person("Janet")
   add_person(janet)
   add_person(make_person("Porko"))
end

function touch_start(total_number)
   print_people()
end

-- Simulate the state_entry event
state_entry()

Now, you might not really be one to break out all those tiny one-line functions. I am used to writing very large scripts and even larger real programs, and I find that tiny useful functions work well for me. But they do begin to have a drawback, which is that you tend to get a lot of them and they can become disorganized easily, and hard to keep track of.

Well, it turns out that Lua has some interesting capabilities that can help us with that organization. One is that a table can contain not just data but functions, because a function is an object just like any other Lua thing. So we can make our print part of our person. One way of doing that is like this:

function make_person(name)
   local person = {name=name, first=0, found=0}
   person.display = function(person)
      print(`{person.name} found {person.found} eggs and was first {person.first} times!`)
   end
   return person
end

And then in our report, we can just do this:

function print_people()
   for name, person in pairs(people) do
      person.display(person)
   end
end

And we still get the same output:

[05:42] Scores: Janet found 0 eggs and was first 0 times!
[05:42] Scores: Porko found 0 eggs and was first 0 times!

So suddenly our person table has become intelligent: it knows how to display itself.

But it’s a bit awkward having to say person.display(person). Lua has a special feature to deal with that, the word self and the punctuation mark colon (:). Let me change the code and then explain further.

function make_person(name)
   local person = {name=name, first=0, found=0}
   person.display = function(self)
      print(`{self.name} found {self.found} eggs and was first {self.first} times!`)
   end
   return person
end

That much just looks like we renamed person to self and in fact that is what I did. But now we can say this:

function people.display(people)
   for name, person in pairs(people.contents) do
      person:display()
   end
end

Notice that instead of needing to say person.display(person), now we can just say person:display() with the colon instead of dot. When we do that, SLua automatically and silently passes the object to the left of the colon to the function as the first argument, which, by convention, is called self.

And, to make doing that even easier, Lua lets us say this:

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

Notice that we declared the function here with colon instead of dot, and didn’t include self as an explicit parameter, Lua defines it for us and we can use it as before.

Let’s review what’s going on here: there’s a lot going on in a few lines of code. Once we’re familiar with these things, the power of those few lines will be of value to us, but at first it may seem odd, hard to understand, and not all that useful. Here’s our person table and our reporting code once more:

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

function people.display(people)
   for name, person in pairs(people.contents) do
      person:display()
   end
end

What I like about this code is that the person knows how to display itself, rather than having code somewhere else that knows how to display a person. In this situation if we change person, the display code is right there waiting to be changed. We don’t have to look for it who knows where.

And later, like tomorrow, when we have forgotten all about how this program works, everything about the person is right here to be understood. Again, it’s not sprinkled all around.

There are a number of things that we might not like about our scheme so far, even if we were the kind of person who likes packaging up behavior (functions) together with their associated data.

Every time we create another person with make_person, we will stick a copy of the very same function into that person’s table. And, though it may not be obvious, it won’t even be the identical function, it will be a new function just like the other ones. Since every person’s display function is essentially the same, it would be good if they were all identical, not copies taking up space. And it would be good if we didn’t even have to waste the extra memory for a pointer to the function in every person table.

It turns out that we can do that. We’re working up to it. Next time we’ll probably get there. Let’s sum up what we’ve learned here.

Summary

A table can contain (pointers to) functions, not just strings and numbers and such. Those functions can refer to the other elements of the table using a Lua-provided variable self, so the embedded functions can process the fields of each specific instance of a table with that shape.

Even with just this much capability, we can see how we can associate our functions with the data that they process, keeping like things together. We can use this capability to help us organize our larger scripts. With practice, even just this much capability could pay off in easier changes and fewer mistakes.

But I’m not here to recommend that you start organizing your code like this quite yet, though you might want to practice with the ideas a bit. The next few steps that we take will give us something better.

As always, if you have comments or questions IM me or even email valkyriejanet@gmail.com if you wish. IM is better, probably. But especially if you have ideas on how to improve these articles I do want to hear from you.

Until next time, safe paths!