Using Nasal functions

From FlightGear wiki
Jump to navigation Jump to search


Functions

What is a 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.

In its most basic form, a function does not even take any arguments. Let's imagine a function named hello, functions are typically called by appending parentheses to the function name:

 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. 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(who) {
 print("Hello ", who);
}


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 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.

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.

 var log_message = func {
    print(arg[0]);
 }

Note that this is 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.

As previously mentioned, parenthesis are generally optional, and func, func(), and func(arg...) are identical. However, what is not stated anywhere is there is always an invisible arg... after any argument list, thus func(a) and func(a, arg..) are actually the same! This means that while function's arguments may underflow (have too few), they will never overflow, instead they will just fill in the arg vector. The only way to prevent this is to manually die on the arg vector having any size, which IMO could be a bad thing depending on the way the error is printed (i.e. if they don't show the same thing, like "Nasal runtime error" and "Nasal parse error", but also the fact that the trace-back would not show the right line, as it would show the die() call not the place where it was called from).

As a side note, when arg... is not explicitly declared (e.g. func or func(a)), caller(0)[0] does not show "arg" and yet does not result in an undefined symbol error when I try and return arg. Why is this?

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

Function arguments can have default values, as in C++. Note that the default value must be a scalar (number, string, function, 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 first in the argument list:

#Incorrect:
var log_message = func(msg="error", line, object="ground") { 
  1. some code

}

 #Correct:
 var log_message = func(msg="error", object="ground", line) { 
#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]

Returning from functions

In Nasal, functions return implicitly the values of the last expression (e.g. "nil" in empty function bodies), you can also add an explicit "return" statement, for example to leave a function early. In addition, it is possible to return values, too.

So, semantically, these are all equivalent:

var log_message = func {return;}
var log_message = func {nil;}
var log_message = func {}; 
var log_message = func return;
var log_message = func nil;

Named arguments in function calls

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 more readable.

And it also makes it possible to call function's without having to take care of the right order of passing 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=hil, 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 an 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.

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 to read:

 var add = func(val1,val2) val1+val2;

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]();



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.