Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

OO #5, Very Interesting Idea

Wild idea: What if we didn’t put code in dataserver that knows so much about Person, but instead let the Person handle the dataserver event?

I have found a way of doing just that, and while it is a bit deep in the bag of tricks, it’s really rather straightforward and you’d do it the same way most every time. Here’s what the dataserver code looked like when last we saw it:

function dataserver(queryId, data)
   local person = RezDayQueries[queryId]
   if person then
      person._rez_date = data
   end
   RezDayQueries[queryId] = nil

   local person = PayInfoQueries[queryId]
   if person then
      person._pay_info = data
   end
   PayInfoQueries[queryId] = nil

   if next(RezDayQueries) == nil and next(PayInfoQueries) == nil then
      create_report()
   end
end

Here it is in my new version:

function dataserver(queryId, data)
   local action = Queries[queryId]
   if action then
      action(data)
   end
   Queries[queryId] = nil

   if next(Queries) == nil then
      create_report()
   end
end

There is now just one query table, not two. And the dataserver event doesn’t know anything about what the action is to be taken on the data. Instead, it finds a function in the table at Queries[queryId] and just calls it, passing in the data.

Here’s how the query table entry gets created now. Notice that what we add to the Queries table isn’t just the person instance: it is a function that we get by calling one of two other functions, handle_rez_date() or handle_pay_info(). Read on, but keep in mind that you haven’t yet seen quite what goes into Queries.

function make_people_list()
   People = {}
   Queries = {}
   local peopleScan = ll.GetAgentList(AGENT_LIST_PARCEL, {})
   for _i, uuid in ipairs(peopleScan) do
      local person = Person:new(uuid)
      table.insert(People, person)
      Queries[person:request_rez_date()] = person:handle_rez_date()
      Queries[person:request_pay_info()] = person:handle_pay_info()
   end
end

Those last two statements used to put one entry into the rez date table and one into the pay info table. Here we just put two entries into the single Queries table. T

he old code put the person instance into the table. Now we are putting the result of calling one of two handler methods, handle_rez_date or handle_pay_info.

What are the results of those two handle methods? Each one returns a function, provided like this:

function Person:handle_pay_info()
   return function(data)
      self._pay_info = data
   end
end

function Person:handle_rez_date()
   return function(data)
      self._rez_date = data
   end
end

Note that the functions accept the data (from the dataserver) as a parameter. and they store the data into the appropriate private member variable, _pay_info or _rez_date. We’re not returning the result of the function, we are returning the function itself. This code doesn’t call it: the code in dataserver that says action(data) calls it.

So the make_people_list code puts two unique functions into the Queries table for each Person we create, one to store the payment info and one for the rez date. The dataserver, no matter what has come back, finds the function that matches the query id and calls the function, which stores the data in the right place.

Now, I think we can make this a bit simpler and more clear with a bit more work, but what I see already is that all our dataserver event code from this day forward could look almost exactly like it does now, no matter how many different dataserver queries we have to set up. We’d have to do something about what happens when the queries are exhausted, rather than call create_report. Perhaps we would set up a standard callback for that case as well. It will take more experience and more trials to be sure, but the handling of the specific data can, I think, always be delegated to the object making the request. And I think that will be quite nice.

Summary

A very few lines of code in our Person object have allowed us to reduce the dataserver code almost in half. The approach seems likely to work for any object that needs dataserver, and a similar approach can be used for other asynchronous events. In fact, we might even find a use for this idea with ll.Listen, allowing different objects in our script to deal with their concerns separately, without the need for a lot of conditionals in the listen event. I’m looking forward, so I could be wrong, but this idea seems promising to me and I plan to push forward with it when it makes sense to do so.

The fundamental “trick” here is that a function in Lua is a “first-class object”, just like an integer or a string or a Person object. As such, it can be passed around, stored away, and called at a later time. This notion is similar to the lambda that you’ll find in other languages, often seen in functional programming. There are a lot of pitfalls to passing functions around, and it definitely requires care and practice, but that’s really no different from any other aspect of programming, is it? It’s a powerful tool, to be used with care, and once we find a good way of using it, we should stick pretty close to that way.

I suspect that this idea will evolve a bit as I learn more, but for now, I think it has made a noticeable improvement to the program and that it will find a place in my Lua toolkit.

Questions and comments are welcome, in Second Life, of course.