Janet's Notes

Miscellaneous Articles Trying to be Useful


Project maintained by JanetRossini

Function Calls

Now let’s look at special SLua ways of calling functions, with optional arguments and variable lists of arguments. In this article, we’ll show how to handle an optional argument.

Suppose we wanted a function to scale numbers by some factor. We might write this test:

   assert_equals(scale(2, 5), 10)
   assert_equals(scale(2, 7), 14)

And we could code scale like this to pass the test:

function scale(value, factor)
   return value * factor
end

And our tests would pass. We are up to 11 now, because I’m still running all the tests from the previous article:

[12:18] JR20250501: Tests: correct 11, wrong 0

But suppose we wanted our scale function to default to a scale of ten. We might write this additional test assertion:

   assert_equals(scale(2, 5), 10)
   assert_equals(scale(2, 7), 14)
   assert_equals(scale(7), 70)

If we run now, we’ll get an error:

JR20250501 [script:testing and stuff] Script run-time error
runtime error
lua_script:33: attempt to perform arithmetic (mul) on number and nil
lua_script:33 function scale
lua_script:94 function tests
lua_script:61 function run_tests
lua_script:3 function state_entry
lua_script:100

That’s because the second argument to scale was not provided and is therefore nil. So we could change our function like this:

function scale(value, factor)
   if factor == nil then
      factor = 10
   end
   return value * factor
end

In the code above we just check to see if factor is nil, which it will be if it isn’t provided. And if it is nil, we set it to 10. Now all our assertions pass:

[12:23] JR20250501: Tests: correct 12, wrong 0

It turns out that there is a shorter way of doing that check, using SLua’s logical operator or, like this:

function scale(value, factor)
   factor = factor or 10
   return value * factor
end

This is the approved way to handle this situation in Lua and SLua, so we need to understand it. First, we’ll consider a simple logical expression. Suppose we want to do something if age is greater than beauty or weight is less than 100. In LSL we might write:

if ( age > beauty || weight < 100 ) {
   do_something();
}

In SLua we write that same thing like this:

if age > beauty or weight < 100 then
   do_something()
end

LSL uses || to mean “or” and Lua uses or to mean “or”.

But there is one important difference between the two. In Lua, when you are doing an or, if the first thing is true, you just use it and don’t evaluate the second part. (And, it turns out, if you are doing an and and the first bit is false you don’t evaluate the second part.) There’s no point to evaluating the second part because we already know how it has to turn out because true or false is true, and false and true is false. And there’s still another Lua oddity that we have to consider. In Lua only nil and false are false. Every other value, even zero, evaluates as true.

So when factor is provided, like this:

   assert_equals(scale(2, 7), 14)

Inside the function, factor is 7 so this code:

factor = factor or 10

Is the same as

factor = 7 or 19

7 evaluates as true (we say that 7 is “truthy”) so the second part of the or does not run and the boolean expression returns the first “true”, namely 7. So factor is 7. But in this call:

   assert_equals(scale(7), 70)

The value of factor is nil, so we evaluate

factor = nil or 10

And since nil is false, we evaluate the second part of the or, which is 10, which is truthy, so we return it. And we set factor to 10 as intended.

Now a reasonable person might ask why we even allow things like 10 to be truthy and to be honest, I do not know. But that’s how it works, and since 10 is truthy, it’s a perfectly good result from the boolean, and the shorthand works as we intend.

My advice would be just to learn that notation and go with it, and if you felt like using the longhand version, I wouldn’t be offended. But this short form is kind of the official cool kids way to do it.

I think we’ve said enough on this topic, and it’s enough to take in at one go. I’ll just add that we could check more than one optional argument if we wanted to, but it starts getting pretty strange with more than one optional, at least in my view.

One more thing. If I wanted a function with a lot of optional arguments (and I can’t remember ever wanting that), I would very seriously consider passing in a table of arguments rather than just a straight call. Something ike this:

function long_name(args)
   title = args.title or ""
   first = args.first or ""
   middle = args.middle or ""
   second_initial = args_second_middle or ""
   last = args.last or ""
   -- do whatever we do
end

Then we might call it in these forms:

long_name({first="Janet", last="Rossini"})
long_name({
   title="The Honorable", 
   first-"Jorge",
   middle="F",
   second_initial="X",
   last="McGruder"
})

Again, this is not a thing I recall needing, but if I did, I’d consider something like the above.

Next time, I think we’ll do variable argument lists, called “variadic functions” to confuse us. Safe paths!