Using Nasal functions
The FlightGear forum has a subforum related to: Nasal Scripting |
Nasal scripting |
---|
Nasal internals |
---|
Memory Management (GC) |
(func() {
print("Hello World");
})
();
What is a named function? A "function" is a piece of code that can be easily used repeatedly (without repeating the same code over and over again using your editor's copy&paste function), this is achieved by associating a symbolic name with the piece of code, such as "print", "show" or "get", setprop, getprop for example - here's a simple function that squares a number:
var square = func(number) {
return number*number;
};
This can be interpreted as:
- create a new variable named 'square'
- assign a function to it
- make the function accept a single argument (parameter), named number (the part in parentheses)
- begin/open the function body (the opening curly brace)
- return the result of number*number to the caller
- terminate the expression using the semicolon
- close the function body (the closing curly brace)
- and terminate the expression via the semicolon
So, to put things in layman's terms, imagine your function like a recipe in a cooking book - instead of always having to write down the recipe, you just open the cooking book, "jump" to the corresponding page, "execute" the recipee and then "return" to where you left off earlier.
You can also image a function like your telephone's automatic redial function: You only need to specify the job once (that is, type in the number) and then you can always execute the "program" (the number) easily by calling the corresponding redial program.
Conceptually, a function works pretty much like that: it has a starting point where its instructions start, and prior to going there, it will note down where to continue afterwards - that way, it is possible to call functions in a nested fashion, i.e. in terms of our previous cooking analogy, just imagine a sophisticated meal with lots of different dishes, where each dish would be represented by a different function, and where certain steps may use "helper function" (read: recipes).
Simple examples
Note if you don't understand nested (anonymous) functions yet, use explicit function names:
var myFunction = func() {
print("Hello World!");
}
myFunction (); The main difference being that the invocation of the myFunction function can be replaced with the anonymous function body, i.e. without any function name associated with it. |
In its most basic form, a function does not even take any arguments. Let's imagine a function named hello:
var hello = func() {
print("Hello !");
};
To actually call the function, you can use the call() API, which expects
- the function name
- a vector with arguments:
var hello = func() {
print("Hello !");
};
call( hello,[] );
The call() API provides the greatest degree of flexibility, because it allows you to directly specify:
- function (func)
- arguments (vector)
- object (me reference/hash)
- namespace (hash)
- vector for list of errors (if any)
But, usually, Functions are just called by appending parentheses to the function name, empty parentheses for function calls without any arguments:
hello()
As a single statement, you'd want to terminate the instruction using a semicolon:
hello();
However, you do not always need to terminate a function call, especially not embedded calls, imagine this:
hello( hello() );
This will first call the inner hello() function, and afterwards the outer function. Note how only the outer function call has a terminating semicolon.
You may be wondering what's happening here by calling the hello() function this way, in a nested fashion. So called 'void' functions that don't return any values, will implicitly return nil to the caller() - so the inner hello() call would return nil to the outer hello() call, which is equivalent to calling hello(nil).
The truth is, functions don't always have empty parentheses - the parentheses are there to pass function arguments (parameters) to the function:
hello( "FlightGear" );
This passes a single function argument to the hello function. In our previous example, we simply used the return value of the first (inner) function call as the argument of the outer (second) call.
Imagine having a piece of code that multiplies two vectors of numbers with each other. Now, if you wanted to use this code in different places, you would have to repeat (copy/paste) the same code over and over again. This is where we instead assign a symbolic name to the code we want to re-use and wrap it in curly braces, the implementation of the hello function (its function body) may look like this:
var hello = func( name ) {
print("Hello ", name);
};
Whenever this symbolic name is then used in the program, the program will "jump" to the definition of the function and start running it, once the called function has completed it will automatically return to the instruction following the call.
By using so called "function arguments" (see below) it is possible to parametrize a function (using variables) so that it may use data that is specific to each call/invocation.
As you can see, the hello function takes a single argument named name' and then runs the print function which prints two words: 1) Hello and 2) the name specified when calling hello().
As previously shown, Nasal functions are implemented using the func keyword, The following snippet of code defines a new function named "log_message" with an empty function body (the curly braces).
var log_message = func {}
Function bodies
To add a "function body", you need to add code in between these curly braces. This code can pretty much be anything at all, and in addition to using global variables and local ones that it has defined, the code can use local variables that represent the function arguments.
Returning from functions
In Nasal, functions return implicitly the values of the last expression (e.g. "nil" in empty function bodies, or when using a single return; expression), and you can also add an explicit "return" statement, for example to leave a function early. In addition, it is possible to return values, too. Whenever a function's signature is empty, because it doesn't take any named arguments, the parentheses are optional. The terminating semicolon after a func block is always optional:
So, semantically, these are all equivalent:
var log_message = func() {};
var log_message = func() {}
var log_message = func {return;} # the semicolon here is also optional
var log_message = func return;
var log_message = func {nil;} # and here too, of course
var log_message = func {};
var log_message = func return; # look, no braces! the function ends at the semicolon
var log_message = func nil;
var log_message = func() nil;
var log_message = func; # emptiness is equivalent to "nil"
Defining function arguments
As shown above, the function arguments belong in parentheses after the "func" keyword. There are some different semantics available for defining and customizing these arguments.
Anonymous function arguments
In Nasal, arguments are by default implicitly passed in the "arg" array, not unlike perl. To understand how this works, you should probably first read up on Nasal vectors, which is just a fancy word for a list of things, that can be individually addressed by appending an index number in square brackets:
var log_message = func {
print(arg[0]);
}
Note that this is basically equivalent to:
var log_message = func() {
print(arg[0]);
}
In other words, the argument list "()" can be omitted if it is empty. However, if you are new to Nasal or programming in general, it is probably a good idea to ALWAYS use parentheses, i.e. also for functions with empty argument lists - that makes it easy to get used to the syntax.
Note that this is just an assignment of an (anonymous) function argument to the local "log_message" variable. There is no function declaration syntax in Nasal.
Also, Nasal being a functional programming language, all passed arguments will be local to the corresponding scope. If you want to modify state in a function, you'll preferably return new state to the caller.
Named function arguments
You can also pass named arguments to a function, thus saving the typing and performance costs of extracting them from the arg array:
var log_message = func(msg) {
print(msg);
}
The list of function arguments is called a function's "signature".
Default values for function arguments
Named function arguments can have default values, as in C++ or Python. Note that the default value must be a constant (number, string, function, or nil) and not a mutable composite object (list, hash).
var log_message = func(msg = "error") {
print(msg);
}
If some arguments have default values and some do not, those with default values must come last in the argument list:
#Incorrect:
var log_message = func(msg = "error", line, object = "ground") {
#some code
}
#Correct:
var log_message = func(line, msg = "error", object = "ground") {
#some code
}
Any extra arguments after the named list are placed in the "arg" vector as above. You can rename this to something other than "arg" by specifying a final argument name with an ellipsis:
listify = func(elements...) { return elements; }
listify(1, 2, 3, 4); # returns a list: [1, 2, 3, 4]
Calling a function with arguments
At the very beginning, you saw that function arguments, when calling a function, go inside the parentheses after the function name. No arguments means nothing there. As with defining arguments, there are still a few ways to specify the arguments. Note that you can only use one or the other: unlike Python, the two methods do not mix well.
Positional arguments
The most commonly used method for specifying arguments is a simple comma-separated list of expressions, which get mapped to the named arguments, overflowing into the "arg" symbol if needed (or the argument declared with ... after it). Here's a series of examples illustrating this:
# Repeated from above: a simple function with a defaulted argument
var log_message = func(msg="error") {
print(msg);
}
log_message("Hello!"); # the first specified argument gets mapped to the first declared argument, thus: msg="Hello!"
log_message(); # no specified arguments, so the default is used: msg="error"
var print_arg = func {
foreach (var argument; arg)
print(argument);
}
print_arg("Hello!", "How are you today?") # there are now two arguments and they are absorbed into the implicit "arg" vector, thus: arg=["Hello!", "How are you today?"]
print_arg() # no arguments, so arg=[] and nothing is done: print(argument) is not even run
Named arguments
Nasal supports named function arguments in function calls, too. As an alternative to the comma-separated list of positional function arguments, you can specify a hash literal in place of ordered function arguments, and it will become the local variable namespace for the called function, with variables named according to the hash indexes and with values according to the hash values. This makes functions with many arguments - or obscure ones - more readable.
And it also makes it possible to call function's without having to take care of the right order of passing arguments, which comes in especially handy once you have to work with code that you haven't touched in a while. Basically, it's a really good idea to always make use of this whenever a function is non-trivial, in that it may take more than just 2 straightforward arguments.
Examples:
#if we have functions defined:
var log_message = func (msg="") {
#some code to log variable msg
}
var lookat = func (heading=0, pitch=0, roll=0, x=nil, y=nil, z=nil, time=nil, fov=20) {
#some code using those variables
}
#we can use them them the usual way with comma separated list of arguments:
log_message("Hello World!");
lookat(180, 20, 0, XO, YO, ZO, now, 55);
#or we can use the hash literal arguments instead:
log_message(msg:"Hello World!");
lookat(heading:180, pitch:20, roll:0, x:X0, y:Y0, z:Z0, time:now, fov:55);
Both methods for calling the functions above are equivalent, but note the the second method is more readable, less prone to error, and self-documenting in the code for the function call.
As another example, consider:
var setPosition = func (latitude_deg, longitude_deg, altitude_ft) {
# do something here
}
# the actual function call:
setPosition(latitude_deg:34.00, longitude_deg:7.00, alt_ft:10000);
In other words, such function calls become much more self-explanatory because everybody can see immediately what a value is doing. This is a good practice, as you may eventually have to take a longer break, away from your code - and then even you yourself will come to appreciate such small things that make code more intuitive to work with.
Declared arguments are checked and defaulted as would be expected: it's an error if you fail to pass a value for a mandatory/undefaulted argument, missing default arguments get assigned as usual, and any rest parameter (e.g. func(a,b=2,rest...){}) will be assigned with an empty vector.
Specifying parameters at call-time
The previously discussed syntax also makes it possible to have functions without any named function arguments in their signature, while specifying arguments only during invocation:
var print_pos = func {
print('lat:', lat, ' lon:', lon, '\n');
}
print_pos(lat:43.94, lon:23.88); # these show up perfectly fine as lat=43.94 and lon=23.88
Note how the function's signature doesn't have any named parameters at all - instead, the function call sets up a hash that is added to the namespace during invocation.
You can use this behavior to implement generic constructors that support object-specific instantiation, but also to implement function overloading - based on different parameter names at the call sites.
Also note that the standard arg vector, including its ellipsis form, is no longer available when using a hash to initialize the parameters of a function call.
Argument passing by value / by reference
If you pass an argument to a function you must consider its type (see Typeof). If you pass a scalar (string, number), it will be passed by value, if you pass a hash or vector, it will be passed by reference.
This means:
- If you change an argument variable in your function that was passed by value (copied), the variable outside the function will stay unchanged.
- If you change an argument variable in your function that was passed by reference (same memory), the value outside the function will change.
var add1 = func(myScalar) {
myScalar = myScalar + 1;
return myScalar;
}
var set42 = func(myVector) {
myVector[0] = 42;
}
var a = 1;
print(a);
print(add1(a)); # this prints the return value of the function call!
print(a); # variable a outside the function shall be still 1
var b = [1,2,3];
print(debug.string(b));
print(set42(b)); # print the return value which is 42
print(debug.string(b)); # now b[0] has changed
Nested functions, implicit return
Also, Nasal functions can be easily nested, for example:
var calculate = func(param1,param2,operator) {
var add = func(p1,p2) {p1+p2;}
var sub = func(p1,p2) {p1-p2;}
var mul = func(p1,p2) {p1*p2;}
var div = func(p1,p2) {p1/p2;}
if (operator=="+") return add(param1,param2);
if (operator=="-") return sub(param1,param2);
if (operator=="*") return mul(param1,param2);
if (operator=="/") return div(param1,param2);
}
Note that the add,sub,mul and div functions in this example do not make use of an explicit return statement, instead the result of each expression is implicitly returned to the caller.
Nasal functions that just consist of such simple expressions can also be further simplified by ommitting the braces:
var add = func(p1,p2) p1+p2;
Function overloading
Note that Nasal functions can generally not be overloaded, and that operator overloading in particular is also not supported.
However, the effects of function overloading can obviously be implemented individually by each function, simply by processing the number and type of passed arguments at the start of the function body. The FlightGear code base contains a number of examples for this, i.e. it is for example possible to pass properties in the form of plain strings to a callback or in the form of a Nasal wrapper like props.Node.
So this can be accomplished by first checking the argument count and then the types of arguments passed to the function.
To provide an example, here's a simple function to multiply two numbers, no matter if they are provided as scalars, as a vector or as x/y members of a hash:
var multiply2 = func (params) {
if (typeof(params)=="scalar") return params*arg[0];
if (typeof(params)=="vector") return params[0]*params[1];
if (typeof(params)=="hash") return params.x*params.y;
die("cannot do what you want me to do");
}
So, now you have a very simple form of an "overloaded" function that supports different argument types and numbers:
multiply2( 2,6); # multiply two scalars
multiply2( [5,7] ); # multiply two scalars stored in a vector
multiply2( {x:8, y:9} ); # multiply two scalars stored in a hash
You could obviously extend this easily to support an arbitrary number of arguments by just using a for loop here.
As you can see, the basic idea is pretty simple and also scalable, you could easily extend this to and also return different types of values, such as vectors or hashes. This could for example be used to create wrappers in Nasal space for doing 3D maths, with vectors and matrices, so that a matrix multiplication could return a new matrix, too.
Another common technique to "overload" a function is to get a handle to the original function and then re-implement the function at your script's scope, so that calls to settimer/setlistener will automatically invoke your own version of the function, this is explained at Developing and debugging Nasal code#Managing timers and listeners.
Storing Functions
Functions can also be easily stored in vector and/or hashes:
var hash = {};
hash.hello = func {
print ("Hello !");
}
var vector = [];
append(vector, func {
print("Hello !");
});
##
# call the functions:
##
# the hash call:
hash.hello();
##
# the vector call:
vector[0]();
Function closures
Each time a func {...} expression is evaluated, a new function object is created that is bound both to the namespace that it was evaluated in and to the currently executing function, which in turn is bound to an outer namespace and possibly another function/closure. This creates a list of namespaces, or closures, that represents the different possible scopes for a symbol name: a symbol can either be resolved in the local namespace of the function (created while executing that function) or in one of the successive closures. Since a closure is just a reference to a hash (the namespace), an outer closure may still exist even if it isn't accessible from a global namespace or the local namespace of an executing function (think stack frames in C), since it is kept accessible by the reference from the function object. For example:
var outer = {};
var generator = func(mid) {
return func(local) {
debug.dump(outer);
debug.dump(mid);
debug.dump(local);
}
}
generator([[],[]])(99); # prints {} [[],[]] 99
As evidenced by the example, a function can remember the local namespace of the function that created it (e.g. the local namespace of generator) and can also remember the same closures that the generator had (which in this example would be both the namespace that contains the outer variable as well as the global namespace, whatever that might be).
Misuse of closures
What's wrong this this code?
var result = [];
for (var i=0; i<10; i+=1)
append(result, func print(i));
foreach (var fn; result)
fn(); # should print the numbers 0-9
The problem is that each function generated using this method keeps a reference to the namespace as its closure, not a copy of the namespace. This means that when looking up the symbol i from one of the functions, its current value is used, not its value at the time that the function was made. Thus as the variable i gets modified by the for loop, calling one of the generated functions will print that value of i – and as i is 10 after the end if the loop, we will just see 10 printed 10 times.
How to fix this? The obvious fix is to keep a reference to a copy as the closure and there are two ways to make that happen. The first way is using a generator function, and the second way uses bind(). The first way is really simple:
for (var i=0; i<10; i+=1)
(func {
# this is an anonymous closure that will be able to "save" the value of i
var i = i; # make a copy as a local variable
append(result, func print(i)); # this function will use the local i that we saved
})(); # call immediately
Or one could write it like this (which I prefer for minute reasons related to the garbage collector/GC):
# arguments are local variables, so this form can do the same thing:
var generator = func(i) return func print(i);
for (var i=0; i<10; i+=1)
append(result, generator(i));
The other way (which produces a nearly identical result) uses the builtin bind() function. When a func {...} expression is evaluated, there are two Nasal variables that are involved: one is a naCode and the other is a naFunc. The naCode is stored as a constant for the function and it stores information about the arguments and the body; the naFunc is simply a wrapper that makes the naCode executable and gives it a closure. Each time the VM evaluates the func expression, this creates another naFunc from the naCode base with a unique closure. If you call bind(fn, caller(0)[0], caller(0)[1]) (where fn is the function you just created), you essentially do nothing – as the naFunc is, by default, bound to the caller like so. However, changing the second argument to a different hash, say a unique one for each iteration, will change the outer namespace of the function while keeping the rest of the closure intact (this last part is important for keeping the global namespace as a closure, so that the print function can be found). For example, using an empty hash with our first example snippet and bind() will produce an undefined symbol error, since we effectively removed the closure that contained the i variable. But if we create a hash such as {i:i}, then we will create a faux-namespace with the i variable – but this variable won't be affected by the for loop, and thus we will have accomplished our goal. This is the completed code:
for (var i=0; i<10; i+=1)
append(result, bind(func print(i), {i:i}, caller(0)[1]);
(for the record, bind returns the function it just rebound, even though it works via mutation, so essentially it is a convenience return of the first argument.)
var timers = []; foreach(var delay; [2.0, 4.0, 8.0] ) { #this declares an anonymous/unnamed function (func(arg...) { var interval=delay; var myTimer = maketimer(interval, func() { print("Hello from the ", interval,"-second timer!"); }); # end of the embedded func myTimer.start(); append(timers, myTimer); })(); # this will directly invoke the unnamed function } # end of the foreach loop # kill all timers maketimer(60, func() { foreach(var t; timers) t.stop(); });
Functional programming, higher order functions, generators (advanced concept)
As previously mentioned, arguments to a Nasal function can also be functions themselves (Nasal being a functional programming language), this means that Nasal functions are higher order functions so that you can easily pass and return functions to and from Nasal functions. This can for example be used to dynamically create new functions (such functions are commonly called 'generators'):
# a function that returns a new custom function
var i18n_hello = func(hello) {
return func(name) { # returns an anonymous/unnamed function
print(hello,name);
}
}
# create three new functions
var english_hello = i18n_hello("Good Day ");
var spanish_hello = i18n_hello("Buenos Dias ");
var italian_hello = i18n_hello("Buon giorno ");
# actually call these functions
english_hello("FlightGear");
spanish_hello("FlightGear");
italian_hello("FlightGear");
Using helper functions
It is possible to simplify complex function calls by introducing small helper functions, for example consider:
var l = thermalLift.new(ev.lat, ev.lon, ev.radius, ev.height, ev.cn, ev.sh, ev.max_lift, ev.f_lift_radius);
So, you could just as well create a small helper function named"thermalLift.new_from_ev(ev)":
thermalLift.new_from_ev = func (ev) {
thermalLift.new(ev.lat, ev.lon, ev.radius, ev.height, ev.cn, ev.sh, ev.max_lift, ev.f_lift_radius);
}
var l=thermalLift.new_from_ev(ev);
Note that the expression to invoke your code would then also become less complicated and much more comprehensible.
When you have expressions of nested method calls, such as:
t.getNode("latitude-deg").setValue(f.getNode("latitude-deg").getValue());
t.getNode("longitude-deg").setValue(f.getNode("longitude-deg").getValue());
You could just as easily introduce a small helper function to wrap the code, that would be less typing for you, less code to read (and understand) for others and generally it would help localize functionality (and possible errors):
var copyNode = func(t,f,path) t.getNode(path).setValue(f.getNode(path).getValue());
So you would simply take the complex expression and generalize it by adding variables that you pass in from a function object, then you could simply call your new function like this:
copyNode(t,f,"latitude-deg");
copyNode(t,f,"longitude-deg");
or:
foreach(var p; ["latitude-deg", "longitude-deg","generated-flag"])
copyNode(t,f,p);
or as a complete function accepting a vector of properties:
var copyNode = func(target,source,properties) {
if (typeof(properties)!="vector") properties=[properties];
if (typeof(target)!="hash") target=props.globals.getNode(target);
if (typeof(source)!="hash") target=props.globals.getNode(source)
foreach(var path; properties)
target.getNode(path).setValue( source.getNode(path).getValue() );
}
copyNode("/temp/test", "/position", ["latitude-deg", "longitude-deg", "altitude-ft"]);
Whenever you have very similar lines of code that seem fairly repetitive, it is a good idea to consider introducing small helper functions. You can use plenty of small helper functions and then just "chain" them together, rather than using complex nested expressions that make your head spin.
Another way to call functions
... using call(). Sometimes, you may need to more finegrained control over functions that you want to call, such as specifying an object for a method call, or for exception handling purposes. This is accomplished using Nasal's call() API:
call(function, vector_of_arguments, object, namespace, error_vector);
This will call the given function with the given arguments in the vector and returns the result. The only required argument is the function (and vector of arguments respectively), all other arguments are optional and defaulted to nil.
The optional arguments can be used to specify the "me" reference for a function call (object) and the local variable namespace. The error argument, if present, must be a vector. If the called function terminates due to a runtime error (exception) or die() call, the error (either a string or the argument to die() is appended to the vector, followed by the file name at which the error occurred, followed by the line number in that file, followed in order by each file/line number on the call stack.
To learn more, see [1].