Nasal Variables

From FlightGear wiki
Jump to navigation Jump to search


A variable is a combination of a name/namespace (e.g. foo in the local namespace or setprop in the global namespace) and a value (from simple scalars, like 1 or "hello!", to more complex data types, like hashes and vectors). Here we will explain some of how the various types of variables work.

Variables

Nasal scripts should make use of the var keyword when declaring variables. The var keyword makes a variable guaranteed to be local. Nasal natively provides support for scalars (numbers, strings), lists (arrays, vectors) and hashes (objects or dictionaries); more complex data structures (such as trees) can be built using vectors or hashes.

vectors and hashes can be understood as containers for your data; in the case of vectors, you end up with a container that has sequentially-numbered places for each element - whereas a hash can also used named keys for its values. Storing happens in a key/value fashion. Data look-up is usually done via square brackets:

# create an empty vector
var myVector = [];

# append a value to it
append(myVector, 10);

# get out the first element (starting at 0)
var firstElement = myVector[0];

Equally, a hash is easy to set up to serve as container for your values:

var myHash = {};
myHash.field = 10;
var myField = myHash.field;

And of course, Nasal also has functions: bits of code which take inputs and can return values and that can be "called" again and again. Rounding out the types are nil (aka NULL pointer) and ghost (Garbage-collected Handle to OutSide Thingy – a virtual type that represents a C or C++ object). For more see the table of types. Here's an example of a few of these types, being assigned to symbols:

 var w=100;     # w is a local numerical variable
 var x="hello"; # x is a local string variable
 var y=[];      # y is a local vector (array)
 var z={};      # z is a local hash (dictionary or table) - also used for OOP

What is the difference between a vector and a hash? The difference is, elements in a vector are sequentially numbered, in essence each element has a numeric index:

var my_vector = ['A','B','C'];

This initializes a vector with three elements: A, B and C. To access each element of the vector, you would need to use the element's numerical index:

var my_vector = ['A','B','C'];
print (my_vector[0] ); #prints A
print (my_vector[1] ); #prints B
print (my_vector[2] ); #prints C

As can be seen, indexing starts at zero.

Compared to vectors, hashes don't use square brackets but curly braces instead:

var my_hash = {};

Hashes may not just have numerical indexes, but also symbolic indexes as lookup keys:

var my_hash = {first:'A',second:'B',third:'C'};

This will create a hash (imagine it like a storage container for a bunch of related variables) and initialize it with three values (A, B and C) which are assigned to three different lookup keys: first, second, third.

In other words, you can access each element in the hash by using its lookup key:

var my_hash = {first:'A',second:'B',third:'C'};
print ( my_hash.first ); # will print A
print ( my_hash.second ); # will print B
print ( my_hash.third ); # will print C

Insert new pairs (or change existing):

var hash = {};
hash.field1 = 1;
hash['field2']= 2;
hash["field3"] = 3

Nasal supports a nil value for use as a null pointer equivalent:

 var foo=nil;

Also, note that Nasal symbols are case-sensitive, these are all different variables:

 var show = func(what) {print(what,"\n");}
 var abc=1; # these are all different symbols
 var ABC=2; # different from abc 
 var aBc=3; # different from abc and ABC
 
 show(abc);
 show(ABC);
 show(aBc);

Please note that functions assigned to variables are no exception. If you write code without using var on variables, then you risk (often hard to debug) breakage at a later time because you may be overwriting symbols in another namespace.

So functions bound to variables should use the var keyword as well:

 var hello = func { 
   print("hello\n"); 
 }

There's another reason why var should be used consequently, even if a variable is safe enough from later side effects, because it has a relatively specific or unique name: The var keyword makes reading code for others (and for the author after some time) easier, as it makes clear: "this variable starts its life *HERE*". No need to search around to see whether assigning a value to it means something to other code outside or not. Also, with an editor offering proper syntax highlighting reading such code is actually easier, despite the "noise".

The problem with nasal code that does not make use of the var keyword is, that it can break other code, and with it the whole system, but no Nasal error message will point you there, as it's syntactically and semantically correct code. Just doing things that it wasn't supposed to do. For a more in-depth discussion, please see Nasal & "var".

Nasal scripts that are loaded from $FG_ROOT/Nasal are automatically placed inside a namespace that is based on the script's name.

For example, referring to our earlier "Hello World" example, global variables defined in the hello.nas script would be accessible by using hello as prefix from other modules:

 # hello.nas
 var greeting="Hello World"; # define a greeting symbol inside the hello namespace

If you were now to read out the value from the greeting variable from another Nasal module, you would have to use the hello prefix:

 # greetme.nas
 print(hello.greeting); # the hello prefix is referring to the hello namespace (or module).

Advanced uses of variables

Nasal also supports Multi-assignment expressions. You can assign more than one variable (or lvalue) at a time by putting them in a parenthesized list:

   (var a, var b) = (1, 2);
   var (a, b) = (1, 2);               # Shorthand for (var a, var b)
   (var a, v[0], obj.field) = (1,2,3) # Any assignable lvalue works
   var color = [1, 1, 0.5];
   var (r, g, b) = color;  # works with runtime vectors too

Vectors (lists or arrays) can be created from others using an ordered list of indexes and ranges. This is usually called vector slicing. For example:

   var v1 = ["a","b","c","d","e"]
   var v2 = v1[3,2];   # == ["d","c"];
   var v3 = v1[1:3];   # i.e. range from 1 to 3: ["b","c","d"];
   var v4 = v1[1:];    # no value means "to the end": ["b","c","d","e"]
   var i = 2;
   var v5 = v1[i];     # runtime expressions are fine: ["c"]
   var v6 = v1[-2,-1]; # negative indexes are relative to end: ["d","e"]

The range values can be computed at runtime (e.g. i=1; v5=v1[i:]). Negative indices work the same way they do with the vector functions (-1 is the last element, -2 is 2nd to last, etc...).

Vector slicing

In Nasal, like many other scripting languages, one can slice vectors, that is, make a new vector gathering from indices of a previous vector. (Note that slicing strings is currently not supported but is possible.) The basic syntax for slicing is this:

vec[n:m];

This makes a new vector who's contents are taken from vec starting at index n and going until index m – including both indices. This contrasts with Python, who considers index m a "one-past-end" index, i.e. it does not include it in the final result. Let's take an easy example to illustrate this:

Start with a vector, call it vec like above, and have it of size o. We will fill each index with the number of that index, like so:

var vec = setsize([], o);
forindex (var v; vec)
    vec[v] = v;

Thus a vector of size 5 would be [0,1,2,3,4]. Let's look at slicing with that vector. Using the notation above, let's take a slice of the whole vector. We of course have to start at the first index – 0 – and we go to the last valid index – which is not o, but o-1. Thus we have:

vec[0:o-1];

In Python, this would actually go to o not o-1, but using subvec() notation, we get the same as Python:

subvec(vec, 0, o);

Be warned that subvec() takes a starting index and length and thus is not equivalent! There are several other ways to notate this as well:

vec[0:-1];
vec[:-1];
vec[0:];
vec[:];

Note the last three, they show that we can leave out an index to designate the start or end of the vector (or we can equivalently put 0 for the start and -1 for the end). If we do an arbitrary slice from n to m, we will get a vector like this (due to the way we filled our indices):

vec[n:m] == [n, n+1, n+2, ..., m-1, m];

A degenerate vector slice becomes a simple subscript inside a new vector:

vec[n:n] == [vec[n]];
vec[-1:] == [vec[-1]];
vec[:0] == [vec[0]];

You can think of vector slicing as designating the two end-caps of the result, with the result including those indices.

Another feature of Nasal's slices is that one can pick individual indices to "cherry-pick" out of the original vector using commas:

vec[0,5,7] #pull indices 0, 5, and 7

This can also be combined with the other form of slices like so:

vec[0,5:7] #0, 5, 6, and 7

Now let's make a range() function.

Making a range function

In Python, the range function returns what is essentially a vector slice of an '"infinite" vector who's indices are filled like we did above. That is, it returns a vector like this:

[arg[0], arg[0]+1, arg[0]+2, ..., arg[1]-2, arg[1]-1]

This turns out to be incredibly useful for iterating over the size of a vector (for i in range(0,len(vec)):) and also has other applications. Since we are doing this in Nasal, we need to be careful with the last index. While in Python it's a equivalent to a simple slice, but in Nasal we need to subtract 1 from the top index. Here's an example of how to do it:

var _range = []; #gets filled like [0, 1, 2, 3, ... ]
var range = func(start, end) { #one-past-end, actually
    # We need to make sure our index end-1 exists,
    # which will happen when the size is equal to end,
    # (since size-1 is always the last valid index)
    while(size(_range) < end) {
        append(_range, size(_range));
    }
    return _range[start:end-1];
};

Nasal variables vs. the property tree

With FlightGear's built-in property tree and Nasal's support for it, there are two obvious, and two somewhat competing, ways for storing scalar data: native Nasal variables and FlightGear properties, both of which can be easily accessed and managed from Nasal.

The advantage to native Nasal-space data is that it's fast and simple. If the only thing that will care about the value is your script, they are good choices.

The property tree is an inter-subsystem communication thing. This is what you want if you want to share data with the C++ world (for example, YASim <control-output> tags write to properties – they don't understand Nasal), or read in via configuration files.

Also, native Nasal data structures are usually far faster than their equivalent in property tree space. This is because there are several layers of indirection in retrieving a property tree value.

In general, this means that you shouldn't make overly excessive use of the property tree for storing state that isn't otherwise relevant to FlightGear or any of its subsystems. Doing that would in fact have adverse effects on the performance of your code. In general, you should favor Nasal variables and data structures and should only make use of properties to interface with the rest of FlightGear, or to easily provide debugging information at run time.

getprop() vs node.getValue()

As of FG 2.4.0, retrieving a value from the property tree via getprop() is about 50% slower than accessing a native Nasal variable, and accessing the value via node.getValue() is 10-20% slower yet. This is an insignificant amount of time if you are retrieving and storing a few individual values from the property tree, but adds up fast if you are storing or retrieving hashes or large amounts of data. (You can easily benchmark times on your own code using systime() or debug.benchmark.)

This is a case where your original Nasal code was the better algorithm: It "ought" to be better to hold the property tree nodes and index them explicitly using getChild() than to create a property tree path string for each child. However, the code that parses property tree paths has been optimized, especially to allocate as little heap storage as possible, so its speed outclasses the Nasal parser [1].

For FlightGear version <= 2.10 the following applies: If there is even a slight performance concern, the only justification for not using getprop() / setprop() directly is if you explicitly require to set a variable type.

Assembling a property path by string manipulation may be in theory less appealing, but it is in practice 3 to 10 times faster than using the props module – I have made several benchmark tests, all leading to the same result. Large-scale property manipulation from Nasal is performance hungry and should be avoided if possible by using Nasal variables instead, and if it needs to be done, getprop() / setprop() offer significantly superior performance[2].

Yes, setprop() / getprop() is definitely faster, because there's less work to do. Evaluating the expression temp1 requires pushing the symbol value (a string) onto the stack, and executing OP_LOCAL which does a hash table lookup to find the value in the namespace list and leave it on the stack for the next bit of code to use.

Evaluating node.getValue() requires:

  1. Pushing the symbol node onto the stack
  2. Executing OP_LOCAL to look it up
  3. Pushing the symbol getValue onto the stack
  4. Executing OP_MEMBER to look it up in the object
  5. Executing OP_CALL to call it as a function
  6. Inside the member function:
    1. Finding the values of me._g and the arg vector: 2 OP_LOCALs and 1 OP_MEMBER (not that expensive, dealing with small hashes)
    2. Looking up the C++/raw variant: another OP_LOCAL and into a densely populated namespace
    3. Finally (!) calling the C++ property node function
  7. Turn the output node into a Nasal object and leave it on the stack.

That said, you really don't want to be designing your scripts around raw, low-level performance issues. Write your code to be readable, not blazingly fast. In general "altitude" is more readable than altNode.getValue() [3].

Note that for FlightGear versions > 2.10 (possibly in 3.0) there's a plan to eventually re-implement/modernize the property tree wrappers using Tom's new cppbind framework[4].

Also have a look to the following mailing list discussions:

  1. setprop() vs. node.setValue()
  2. Canvas display - setprop vs. node.setValue

In addition, it is worth noting that the Nasal/FlightGear APIs cannot currently be considered to be thread safe, this mean that — at least for now — the explicit use of pure Nasal space variables is the only way to exploit possible parallelism in your code by making use of threads.

std.Vector

In FlightGear 3.3 or higher you can manipulate vectors using an object-oriented API by using std.Vector.