Today, we’ll start with my testing “framework” from yesterday, with no tests in it, and we’ll talk about a functions returning nothing, one thing, many things, even tables. Even other functions!
Some aspects of SLua functions are different from what you might be used to in Linden Scripting Language (LSL) or other languages. A function in SLua can return no results, one result, or more than one result. We can imagine reasonable uses for all three cases:
No result: We might have a function that just prints something, or that sends an email. We could write and use it like this:
function send_email(user, message)
ll.Email(user, 'Greetings!', message)
end
-- somewhere deep in our program
user = 'no_one@nowhere.com'
msg = 'We found gold on your land!'
send_email(user, msg)
A function that returns no result was often called a “subroutine” in days of yore. We can write functions without returns in LSL.
One result: This is what we’re used to in LSL when we declare a result type in a function definition. In SLua we do not define the type, we just return a result. Let’s write a test and make it work:
function run_tests()
test_correct = 0
test_wrong = 0
tests()
print(`Tests: correct {test_correct}, wrong {test_wrong}`)
end
function tests()
local double2 = double(2)
assert_equals(double2, 4)
end
If we save the file now, this will happen:
JR20250501 [script:testing and stuff] Script run-time error
runtime error
lua_script:40: attempt to call a nil value
lua_script:40 function tests
lua_script:35 function run_tests
lua_script:3 function state_entry
lua_script:46
Why? Because double
isn’t defined yet. We could arrange our testing framework to deal with that but we’ll save that for another day. Today we’re here about functions and nils and all that. So we write:
function double(n)
return 2 * n
end
And sure enough:
[05:09] JR20250501: Tests: correct 1, wrong 0
Woot! We’re on our way!
More than one result: Suppose that in our program we needed to double a number and also triple it. I’m not sure why, go with me here, OK?
We could write this test:
local double3, triple3 = double_triple(3)
assert_equals(double3, 6)
assert_equals(triple3, 9)
And with this function, we can pass the test:
function double_triple(n)
return 2 * n, 3 * n
end
[05:14] JR20250501: Tests: correct 3, wrong 0
Why 3? Because I kept the double
test and this test makes two assertions. So all three assertions were passed.
So we see that functions can return any number of results, from zero up to as many as you can deal with. Personally, I have used zero and one often, and occasionally two. Beyond two, it’s usually hard to keep track and using an object or table as a return is often a better idea.
Yes, a function can return a table! Why not? Even LSL can return a list, if you want one. Let’s test a table, like this:
local results = two_three_four(4)
assert_equals(results.double, 8)
assert_equals(results["double"], 8)
assert_equals(results.triple, 12)
assert_equals(results.quadruple, 16)
Why did I say results.double
in the one case and results["double"]
in the other? To remind myself, and you, that those two ways of expressing references to a table are just the very same! results.foo
means exactly results["foo"]
, no more and no less.
With this code, our tests all pass:
function two_three_four(n)
local result = {}
result.double = 2 * n
result.triple = 3 * n
result.quadruple = 4 * n
return result
end
[05:24] JR20250501: Tests: correct 7, wrong 0
OK, so we can return zero, one, many values from a function, and we can return a table that contains as many values as we wish. Let’s sit with that a moment, let it sink in. And I’d like to take a moment to talk about these little tests I’m writing. Here’s what I have so far:
function run_tests()
test_correct = 0
test_wrong = 0
tests()
print(`Tests: correct {test_correct}, wrong {test_wrong}`)
end
function tests()
local double2 = double(2)
assert_equals(double2, 4)
local double3, triple3 = double_triple(3)
assert_equals(double3, 6)
assert_equals(triple3, 9)
local results = two_three_four(4)
assert_equals(results.double, 8)
assert_equals(results["double"], 8)
assert_equals(results.triple, 12)
assert_equals(results.quadruple, 16)
end
When I’m learning a language, or when I come across a bit of a language that I’m not quite sure about, or maybe a function call provided by Linden Labs or someone else that I’m not sure about, I often write a little test to determine what the language feature or function does. And when I’m working in the best fashion I know, I have a little testing framework like the one I’ve shown here and that I’m using today.
Why do I think it’s my “best fashion”? Well, because I don’t just test the language, I test the various functions that I use to write my program, writing lots of small functions that I’ve shown to work, combining them to make up whatever program I’m writing. In future articles here, we’ll see some examples of working that way.
In a late-night programming conversation over pizza and beverages, I might suggest that there are kind of two “end points” in programming style. At one end of things, we tend to see all the code that happens on, say, the listen
event, all written out right inside the listen
function. We’d tend to see big functions full of ifs and thens and loops and lots of lovely integer indexing into big strided lists.
At the other end of things, we’d see lots of small functions, and the flow of the program would be rather well expressed by functions calling other functions. Functions would tend to have a single purpose, and to contain only one or maybe two control constructs. The branches of if statements would tend just to call two functions one for true and one for false, instead of having all the code of each branch inside the brackets.
As these articles grow, I’ll try to show some examples of larger programs broken up into small bits. find it a very productive way to work and, if you try it, you might as well. But for now, we have to move on the probably the most weird aspect of SLua functions. A function can return another function that you can call later!
Let’s write a test about that concept, to help ourselves understand it. We’ll work up to it. Remember, we have a function named double
:
function double(n)
return 2 * n
end
What if we wrote this:
local do_it = double
What would that even mean? Well, if double
is a function, now do_it
should be that function.We should be able to call do_it
, just like we can call double
. Let’s write a tiny test to see:
local result
local do_it = double
result = do_it(5)
assert_equals(result, 10)
So in the test above, local do_it = double
sets the variable do_it
to the function double
. You can think of it as setting it to the name of the function if that helps. What it actually is is up to SLua but what we see happens is that when we say do_it(5)
we try to call do_it
and do_it
is set to the function double
, so we wind up calling double
.
Let’s extend our test:
local result
local do_it = double
result = do_it(5)
assert_equals(result, 10)
do_it = triple
result = do_it(5)
assert_equals(result, 15)
That won’t work yet, because there is no function triple
.. yet. So we write it:
function triple(n)
return 3 * n
end
And when we run the program now:
[05:58] JR20250501: Tests: correct 9, wrong 0
The same variable, do_it
, did two different things when we called it. First time, it did double
, second time, it did triple
. Whoa!
Wow. That’s a lot to take in, isn’t it? Functions returning other functions. Setting a regular-looking variable and then just calling it? Wowser, that’s weird.
It’s also very powerful, And like all power tools, it needs to be used carefully, under safe conditions, ideally wearing safety glasses. Our safety glasses in this case look a lot like lots of little tests to be sure that we’re doing what we think we’re doing.
It’s time to close this article, as it is getting rather long. The key notions we’ve encountered include:
Functions can return no result (subroutine), one result, or more than one result.
In my view, more than two gets tricky to handle but you do you.
Functions can return tables.
We simply created a table here, but a function could of course take a table in and produce another table based on that one.
Functions can return other functions.
This may be news to you, depending on your history and background. As we’ll probably see in future articles, returning a function can be very helpful. It can allow us to modify the behavior of our program without sprinkling if
statements all around. We’ve already seen one example of that power, in the earlier article about dataserver
. You might want to take another look at that, better prepared to see how we returned one of two different functions, to handle the two different kinds of returns in dataserver
.
Then again, you might not want to. But I hope you will, especially if that one eluded you a bit.
Next time, we’ll explore another aspect of functions. Not only can they return more than one thing, we can write functions that accept optional parameters, or even variable numbers of parameters.
Remember, though: these are power tools and you can easily hurt yourself. I protect myself as best I can by writing tests. You can find your own way, but I do think small functions, well tested, are a great way to grow larger programs that still work.
Bye for now! Safe paths!