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.
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.