Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

OO #2, A Draft Report

This morning, we’ll start with a very small refactoring, and then produce the first draft of our desired report. At least that’s the plan: plans often go awry.

Refactoring

I was taught to use small functions and methods, and to keep things that relate closely close together and things that do not relate so closely, separated. That’s sufficient reason to refactor this code just a bit:

function touch_start(total_number)
   People = {}
   local peopleScan = ll.GetAgentList(AGENT_LIST_PARCEL, {})
   for _i, uuid in ipairs(peopleScan) do
      table.insert(People, Person:new(uuid))
   end
   for i, person in ipairs(People) do
      print(person:display_name())
   end
end

This code does at least two distinct things: it produces a table of people, and it produces a very trivial report by printing all their names. Let’s break that up this way:

function touch_start(total_number)
   make_people_list()
   create_report()
end

function make_people_list()
   People = {}
   local peopleScan = ll.GetAgentList(AGENT_LIST_PARCEL, {})
   for _i, uuid in ipairs(peopleScan) do
      table.insert(People, Person:new(uuid))
   end
end

function create_report()
   for i, person in ipairs(People) do
      print(person:display_name())
   end
end

Naturally, after any change, I’ll run a quick test by touching the object to see that it prints. And it does.

I’d really prefer a more automatic test than this while I work. Perhaps in a moment we’ll modify the plan and create a little test. It’ll be a bit tricky to do, because our input isn’t known in advance, except that I’ll be in the table of people. We can’t count on anyone else being present.

Let me say a few words about those little functions, make_people_list and create_report. One important thing that they do is to describe what’s going on. In the original code, it would take a few moments of study to work out what was going on. Now, the names give us really good hints. For me, when I’m working with complicated scripts, good names and small functions help me keep track of what’s going on without needing to study and wonder quite so much. I like that.

The Report

This exercise is based on a sample program from the SLua course I’m taking. I’ll provide a copy of that program at some point along this series. What’s important just now si that the program creates an email to send to the a designated address. An email, no surprise here, mostly consists of a big block of text. In the case in hand, that block of text will be a standard report on the properties of each avatar found in the scan. So let’s work in and near the create_report function, to produce a string report, for each Person in our list.

So in create_report, we might create a very long string with a “paragraph” for each Person in the list. Naturally, that paragraph will take the essentially the same form for each Person, just with their values plugged in.

Let’s adjust our create_report like this, to create a (very small) report for each person and print it:

function create_report()
   for i, person in ipairs(People) do
      paragraph = create_paragraph(person)
      print(paragraph)
   end
end

function create_paragraph(person)
   return person:display_name()
end

No change to the output, it still just prints all the names of the folks nearby:

[04:39] OO Scanner: Marie
[04:39] OO Scanner: Janet Rossini
[04:39] OO Scanner: SungManitu
[04:39] OO Scanner: SungAli

Notice that now we have two places where we can make improvements of two different kinds. In create_paragraph, we’ll have all the code about formatting a report for a person, and in create_report we’ll have any code needed to format the whole report, such as headings or spacing between paragraphs or whatever. I like it when that happens, because it helps me to focus on what’s important and not be distracted by nearby code that may or may not need changing.

Anyway, let’s work on the report paragraph. Here is some output from the homework program:

22ba38c9-06e9-4d42-8a6d-495e35bc33b7, janet.rossini, Janet Rossini, 
Shape: 0, Language: 2007-02-24, Rezzdate: 3, Payinfo: 0, 
Position: <155.64500, 33.13590, 23.10590>, rotation: <0.00000, 0.00000, -175.65378>, Complexity: 82673, 
Hover: 0, Height: 1.7728599309921265, Group tag: Valkyrie

We have four lines, each containing a few items. Lines 2, 3, and 4 include names in front of the items, and line 1 does not. So our report will have four text lines and the lines have various items, generally with names, and separated by commas throughout. We’ll generally preserve that format, but we’ll not let ourselves be limited by it if we can do better.

We’re going to get more little functions, and I think I’ll start right out with them, like this:

function create_paragraph(person)
   local lines = {
      line_1(person), line_2(person), line_3(person), line_4(person)
   }
   return table.concat(lines, "\n")
end

function line_1(person)
   return person:display_name()
end

function line_2(person)
   return ''
end

function line_3(person)
   return ''
end

function line_4(person)
   return ''
end

Why do this? Because it gets me close to the whole program in a visible way, and it lets me express my whole idea, which was something like “make a paragraph out of four lines”. The output is correct but with lots of white space because lines 2, 3, and 4 create blank lines so far.

That gives me an idea. I was thinking that I’d just do line 1 then line 2 and so on. Instead, let’s do at least one item on each line, to see how it looks. Line 1 has the display name, which is good for now. Line two starts with shape. I plan to use the SLua back-tick string formatting for the lines so let’s just do the first field names:

function line_1(person)
   return person:display_name()
end

function line_2(person)
   return `Shape: `
end

function line_3(person)
   return `Position: `
end

function line_4(person)
   return `Hover: `
end

Now my report starts to come out, like this:

[05:00] OO Scanner: Janet Rossini
Shape: 
Position: 
Hover: 

And similarly for the rest of the people in the scan, of course.

Let’s complete line_1, so you can see how we do it. We want to display the avatar’s UUID, user name, and display name, with no labels. Like this:

function line_1(person)
   return `{person:uuid()}, {person:user_name()}, {person:display_name()}`
end

Now, I find this nice. In this function, all I have to worry about is the formatting. Getting the data isn’t my concern here. Of course it is my concern in general and I do happen to know that our Person object doesn’t know useer_name yet, so we will write that:

function Person:user_name()
   return ll.GetUsername(self._uuid)
end

After a bit of disappointment that LL didn’t capitalize the N in name, we get this report:

[05:06] OO Scanner: 22ba38c9-06e9-4d42-8a6d-495e35bc33b7, janet.rossini, Janet Rossini
Shape: 
Position: 
Hover: 

We can see now how this is going to go. We extend the string created in whatever line we want to work on, we add any necessary methods to Person, and things get better. There is one thing that is going to happen often: each improvement to a line requires two changes, the formatting one and the operational one, providing the information. When we forget the latter, our program will crash until we provide it.

There are some very fancy things we could do about this. Because we are working with a Person object, we could even set it up to return a default value like “undefined” for any access that didn’t work. In a major program we might do that. Here, we’ll just be careful and test often. Let’s do a couple more fields and then start to deal with the special ones, the ones that need dataserver.

I’d like to get at least some data on each line, so we’ll do this:

function line_2(person)
   return `Shape: {person:shape()}`
end

This demands a shape method, which goes like this:

function Person:shape()
   return ll.GetObjectDetails(self._uuid, {OBJECT_BODY_SHAPE_TYPE})[1]
end

And here’s the new report:

[05:19] OO Scanner: 22ba38c9-06e9-4d42-8a6d-495e35bc33b7, janet.rossini, Janet Rossini
Shape: 0
Position: 
Hover: 

We should talk about that shape() method. It works by passing a single-item list to ll.GetObjectDetails, just the one we want, the shape type. My shape type is 0, standard female.

But we can see how this is going to shape up: we’re going to wind up calling ll.GetObjectDetails once for every field that needs that function to provide information. Wouldn’t it be better to just call ll.GetObjectDetails once per person and get all the fields? It depends what you mean by better. But if we did want to do that, we would call it in the new method, most likely, and we’d then fetch values from the resulting array. Of course we would then have the problem with those array things: we’d need to get the indexes right, since the shape might now be somewhere down in the list.

I generally program for clarity first, and worry about efficiency later. I find that when I keep things well organized, as we’re doing here. it’s not hard to go back and adjust for efficiency later … if we even need to. You, of course, get to do things as you think best. We’re just here to explore this object idea.

I’ll do the first items for the other two lines, with this result:

[05:34] OO Scanner: 22ba38c9-06e9-4d42-8a6d-495e35bc33b7, janet.rossini, Janet Rossini
Shape: 0
Position: <155.64500, 33.13590, 23.10590>
Hover: 0

And of course the code was just like the other cases:

function line_3(person)
   return `Position: {person:position()}`
end

function line_4(person)
   return `Hover: {person:hover()}`
end

function Person:hover()
   return ll.GetObjectDetails(self._uuid, {OBJECT_HOVER_HEIGHT})[1]
end

function Person:position()
   return ll.GetObjectDetails(self._uuid, {OBJECT_POS})[1]
end

I’m inclined to stop here, but I did promise to get a start on the special values that are only available from the dataserver. One of those is the rez date, which comes from ll.RequestAgentData(personId, DATA_BORN). We don’t have time today to actually implement that, but we’ll provide for it like this:

function line_2(person)
   return `Shape: {person:shape()}, Language: {'not implemented'}, RezDate: {person:rez_date()}`
end

Now there are a couple of ways we might define rez_date on Person. One would be to have it just return not implemented, which would be OK. Another way might be to prepare a bit for plugging in the result, by adding a member variable (also called attribute) to Person, set to a default, and later to be filled in. Let’s do that.

function Person:new(uuid)
   local obj = {_uuid=uuid, _rez_date="dataserver"}
   setmetatable(obj, self)
   self.__index = self
   return obj
end

Here, we create a new private member, _rez_date, with a default value of “dataserver”. And our fetching method:

function Person:rez_date()
   return self._rez_date
end

With that in place, my report looks like this:

[05:47] OO Scanner: 22ba38c9-06e9-4d42-8a6d-495e35bc33b7, janet.rossini, Janet Rossini
Shape: 0, Language: not implemented, RezDate: dataserver
Position: <155.64500, 33.13590, 23.10590>
Hover: 0

Summary

I hope you can see how this is going to go. We can now easily fill in all the information that comes from ll.GetObjectDetails, and we’ll be left with two fields saying dataserver until we deal with that issue.

And I guess the main reason that I work this way is that we can see how it’s going to go. Everything works pretty much the same way, with the formatting broken out from the data fetching. Getting the data belongs to the Person, and formatting it belongs to our reporting functions. (We could, and perhaps should, create a separate Reporter object, but that’s beyond the scope of these articles.)

Every new field that we want will be done with the same two simple steps: provide the value, and format the value. And did you notice that we don’t have any of those pesky tostring calls? That’s because the SLua back-tick formatting automatically converts other types to string! Very handy: I’m glad I thought of doing it this way.

Working in this fashion lets me work in very small steps. On a busy day I could do just one new field in the report. Even if I do several, it’s not much trouble to test after each one, even though our testing comes down to clicking the object right now. Perhaps we’ll try to do better in a subsequent article.

But before that, next time we’ll deal with the dataserver. I think we’ll see that it’s pretty easy. Or, I could be wrong and we’ll crash and burn … but I don’t think so.

I hope you’ll keep following along. And, if you have questions, IM me, and I’ll try to help.