Welcome back to another installment of the Lua Unlocked series. In this post, we are
going to dive a bit deeper into tables and functions, and see what we can build when we
combine the two in a certain way.
But first, the usual disclaimer for all of the tutorials in this series:
All methods described here make use of Lua 5.0.x language features, which will not translate
into C++. If you decide to use any of them, you will not be able to make use of ShiVa’s
C++ translator and waive potential performance benefits.
Furthermore, you will greatly confuse the ShiVa syntax highlighter and “compiler”
(syntax checker) in ShiVa 1.9.2. Any undocumented code will not be colored in properly, and
your log will be full of warnings and errors which you must consciously ignore. This can
make debugging “regular” code more complicated.
Variadic Functions
In mathematics and in computer programming, a variadic function accepts a variable number of
arguments. This can be useful for a number of cases where you do not know the number of
arguments passed to a function. A classic example would be text concatenation.
Variadic functions take three dots “…” as arguments, which translate into
an arg table, which must be unpacked:
function lua2.variableParams ( ... )
--------------------------------------------------------------------------------
for i, v in pairs(arg) do
log.message ( v )
end
--------------------------------------------------------------------------------
end
What do you think the output of this call will be?
this.variableParams ( "p1", "p2", "p3" )
Contrary to your intuition, 4 results will be logged: p1, p2, p3, 3, which is a list of all the arguments as well as an addition int for the number of arguments. To prevent the last number from being processed, you need to include the following check:
for i, v in pairs(arg) do
if next(arg,i) ~= nil then
log.message ( v )
end
end
You can also combine the three dots with named parameters, such as:
function lua2.variableParamsPartly ( vVal1, vVal2, ... )
--------------------------------------------------------------------------------
log.message ( "partly defined 1: ", vVal1 )
log.message ( "partly defined 2: ", vVal2 )
for i, v in pairs(arg) do
if next(arg,i) ~= nil then
log.message ( "partly loop: ", v )
end
end
--------------------------------------------------------------------------------
end
A call would look like this:
this.variableParamsPartly ( "v1", "v2", "v3", "v4" )
Without the next(arg,i) check, the last log would be the number of unnamed arguments – so in the call above, that number would be 2, even though we passed 4 total arguments to the function.
Anonymous functions
An anonymous function (often called lambda expression) is a function definition that is not bound to an identifier. If the function is only used once, or a limited number of times, an anonymous function may be syntactically lighter than using a named function. Anonymous functions are ubiquitous in functional programming languages.
Technically, you were using anonymous functions the entire time you have been working with ShiVa: Because in Lua, all functions are technically anonymous. A named function in Lua is simply a variable holding a reference to a function object. These two statements express the same thing:
local result = function timesTwo(x)
return 2*x
end
local timesTwo = function(x) return 2*x end
Since functions are just variables, you can easily put them inside other existing functions:
function lua2.onInit ( )
--------------------------------------------------------------------------------
-- do something here
_return = function () do_something_else end
return _return
--------------------------------------------------------------------------------
end
A tree of single argument functions within a single argument function has a special name and allows for some interesting syntax: Welcome to currying!
Currying
Currying is the technique of translating the evaluation of a function with multiple arguments
into evaluating a sequence of functions, each with a single argument. The name is a
reference to logician Haskell Curry, who also lends his name to the Haskell functional
programming language, where this pattern of programming is much more common.
Take for example the sum of two numbers. A classic function would take both numbers and
return the result:
function lua2.normalSum ( number, anothernumber )
--------------------------------------------------------------------------------
return number + anothernumber
--------------------------------------------------------------------------------
end
The curry version would only take a single argument at a time and evaluate the second argument in its inner anonymous function:
function lua2.currySum ( number )
--------------------------------------------------------------------------------
return function(anothernumber)
return number + anothernumber
end
--------------------------------------------------------------------------------
end
The call signature for both functions is quite different:
this.normalSum (5, 2)
this.currySum (5) (2)
You can “curryfy” existing functions quite easily, using a generic translation function, like this one for any function with 2 arguments:
function lua2.curry ( f )
--------------------------------------------------------------------------------
return function (x)
return function (y)
return f(x,y)
end
end
--------------------------------------------------------------------------------
end
Here is an example of using the curry version of the power function in the ShiVa API:
-- curry
local power = this.curry ( math.pow )
log.message ( power (3) (4) )
Combining lambdas and curry, you could write a specialized and reusable power2-function in 3 lines:
local power = this.curry ( math.pow )
local _pow2 = function(x) return power(x)(2) end
log.message ( _pow2(8) )
However, the real power of this approach comes in the form of partial application.
Partial Application
As you might have suspected, power (3) (4) are actually two function executions, one for the outer function (3), and one for the inner function (4). Since you execute both parts separately, this opens the doors to some really interesting design patterns.
Consider for example a counter function that ticks up by 1 every time it is called. In ShiVa, you would either have to create a local variable at the top of your script which you have to track, or an AI member variable which clutters up your AIModel. With Partial Application, you can write a function like this:
function lua2.localCounter ( )
--------------------------------------------------------------------------------
local i = 0
-- inner lambda
return function ()
i = i + 1
return i
end
--------------------------------------------------------------------------------
end
The variable i is initialized in the outer function, while the counter logic is inside the inner function. If you call the function through Partial Application, i will count up and will not be reset:
local lCounter = this.localCounter ( )
log.message ( lCounter() ) -- i=1
log.message ( lCounter() ) -- i=2
log.message ( lCounter() ) -- i=3
We are effectively executing this.localCounter ( ) ( ) by storing this.localCounter ( ) into local lCounter and then executing the inner function lCounter(). Since the outer function is cached in a local variable, it will not reset when we call the inner function.
Lambdas in Tables
Since our last Lua Table tutorial, you should be familiar with table constructions like this:
local Table = {Apple = "Macintosh", Int_thing = 55, Letters = {a = "a1", b = "b2"}}
log.message ( Table.Apple ) -- "Macintosh"
log.message ( Table.Letters.a ) -- "a1"
Tables can also store references to function objects:
local toolset = { ab = math.abs, si = math.sin }
log.message ( toolset.ab(-35) ) -- 35
log.message ( toolset.si(90) ) -- 1
To make these tables reusable, you can store them inside AI member functions:
function lua2.toolset ( )
--------------------------------------------------------------------------------
return {
ab = math.abs,
si = math.sin
}
--------------------------------------------------------------------------------
end
Then call them like a standard ShiVa function:
local tools = this.toolset ( )
log.message ( tools.ab(-35) )
log.message ( tools.si(90) )
Pseudoclasses
Putting everything we have learned so far together – lambdas, partial application, function objects in tables – we can build something quite interesting: a pseudoclass. There are several approaches to bringing class-like objects to Lua, for instance through metatables, however I found the following method the most convenient for ShiVa.
Let’s start by extending the Partial Application sample with variables, API functions and new self-defined lambda functions, and putting it all inside a table:
function lua2.pseudoClassAI ( )
--------------------------------------------------------------------------------
return {
-- variables
_mStr = "teststring",
_mInt = 55,
-- API functions
_fAbs = math.abs,
_fSin = math.sin,
-- self defined functions
_fSquare = function ( nNum )
return math.pow ( nNum, 2 )
end,
_fCube = function ( nNum )
return math.pow ( nNum, 3 )
end
}
--------------------------------------------------------------------------------
end
You can instantiate and use the class like this:
local PseudoClass = this.pseudoClassAI ( )
log.message ( PseudoClass._mStr );
log.message ( PseudoClass._fAbs(-12) );
log.message ( PseudoClass._fSin(270) );
log.message ( PseudoClass._fSquare(15) );
log.message ( PseudoClass._fCube(32) );
You can instantiate the pseudoclass from the same member function as often as you like. The member variables are not shared:
local PseudoClass2 = this.pseudoClassAI ( )
PseudoClass2._mStr = "derp"
log.message ( PseudoClass._mStr ) -- "teststring"
log.message ( PseudoClass2._mStr ) -- "derp"
Unfortunately, these pseudoclasses would be destroyed when you reach the end of the parent function / frame. To prevent this prom happening, you have to add the class to the global _G table:
rawset(_G, "pseudoG1", this.pseudoClassAI ( ))
rawset(_G, "pseudoG2", this.pseudoClassAI ( ))
pseudoG1._mStr = "first class"
pseudoG2._mStr = "second class"
log.message ( pseudoG1._mStr ) -- "first class"
log.message ( pseudoG2._mStr ) -- "second class"
Now all scripts in your application have access to the pseudoclass you defined.