Howto:Start using vectors and hashes in Nasal

From FlightGear wiki
Jump to navigation Jump to search
This article is a stub. You can help the wiki by expanding it.

Objective: Learn more about vectors and hashes to see how you can simplify and generalize your code easily.

Imagine a piece of Nasal code working with 5 different waypoints. You would need to manage the data for each waypoint. A simple version using different variables for each data set might look like this:

      var wp1 = 0;
      var wp1alt = 0;
      var wp1dist = 0;
      var wp1angle = 0;
      var wp1length = 0;
      var wp1id = "";
      var wp1brg = 0;

      var wp2 = 0;
      var wp2alt = 0;
      var wp2dist = 0;
      var wp2angle = 0;
      var wp2length = 0;
      var wp2id = "";
      var wp2brg = 0;

      var wp3 = 0;
      var wp3alt = 0;
      var wp3dist = 0;
      var wp3angle = 0;
      var wp3length = 0;
      var wp3id = "";
      var wp3brg = 0;

      var wp4 = 0;
      var wp4alt = 0;
      var wp4dist = 0;
      var wp4angle = 0;
      var wp4length = 0;
      var wp4id = "";
      var wp4brg = 0;

      var wp5 = 0;
      var wp5alt = 0;
      var wp5dist = 0;
      var wp5angle = 0;
      var wp5length = 0;
      var wp5id = "";
      var wp5brg = 0;

Now, this is fairly repetitive and not overly scalable, because the variable names are hard coded and need to be changed in a lot of places in the source code.

Just imagine we'd need to support more than just 5 waypoints, like maybe 10, 20, 50 or maybe even 1000 waypoints. As you can see, this method is very inflexible, complicated, tedious and error-prone.

So, it would be better to use a vector of waypoints instead. A vector is a list of things (variables) that can be easily accessed using an index into the vector. Pretty much like an array in C or C++, with the added advantage that the vector can be dynamically resized, e.g. using the setsize() library call. Consider the following example:


var waypoints = ["wp1","wp2","wp3","wp4","wp5"];

This piece of code is equivalent to creating 5 different variables named "waypoints[n]" with an access index from n=0 to 4 (5 elements in total).

As you can see below, indexing starts at 0.

The problem is, that this only gives us a list of single waypoints:

var waypoints = ["wp1","wp2","wp3","wp4","wp5"];
 print( waypoints[0] ); # print wp1
 print( waypoints[1] ); # print wp2
 print( waypoints[2] ); # print wp3
 print( waypoints[3] ); # print wp4
 print( waypoints[4] ); # print wp5

What we really need to save all the other waypoint specific information is a new variable type that serves as the "container" for variables, so that we can save several variables for each waypoint.

var wp4 = 0; # waypoint number
var wp4alt = 0; # waypoint altitude
var wp4dist = 0; # waypoint distance
var wp4angle = 0; #waypoint angle
var wp4length = 0; # waypoint length
var wp4id = "";       # waypoint id
var wp4brg = 0; # waypoint bearing

A vector based version

One simple way to accomplish this is using a another vector for each waypoint, nested inside the original vector. So that we end up with a two-dimensional data structure. For example, imagine a folder containing sub folders, with folders not having names but rather indices.

var wp4 = 0; # waypoint number
var wp4alt = 0; # waypoint altitude
var wp4dist = 0; # waypoint distance
var wp4angle = 0; #waypoint angle
var wp4length = 0; # waypoint length
var wp4id = "";       # waypoint id
var wp4brg = 0; # waypoint bearing

var waypoint4 = [wp4,wp4alt,wp4dist,wp4angle,wp4length,wp4id, wp4brg];
var waypoints = [waypoint4]

First of all, we are setting up all the different variables for wp4, next you are storing all variables in a vector called "waypoint4". In the end, we store this vector in another vector named "waypoints".

So, the very first vector element would be waypoints[0] and it would point to another vector (waypoint4), the elements of waypoint4 would be also available by index:

var wp4 = 1; # waypoint number
var wp4alt = 1000; # waypoint altitude
var wp4dist = 20.4; # waypoint distance
var wp4angle = 33.4; #waypoint angle
var wp4length = 12; # waypoint length
var wp4id = "none";       # waypoint id
var wp4brg = 122; # waypoint bearing


var waypoint4 = [wp4,wp4alt,wp4dist,wp4angle,wp4length,wp4id, wp4brg];
var waypoints = [waypoint4]

print ( waypoints[0][0] ) # contains the data stored in wp4
print ( waypoints[0][1] ) # contains the data stored in wp4alt
print ( waypoints[0][2] ) # contains the data stored in wp4dist
print ( waypoints[0][3] ) # contains the data stored in wp4angle
print ( waypoints[0][4] ) # contains the data stored in wp4length
print ( waypoints[0][5] ) # contains the data stored in wp4id
print ( waypoints[0][6] ) # contains the data stored in wp4brg

What we have here is a list of waypoints, with the first (0th) element containing another vector.

Obviously, you could add a bunch of other waypoints to the "waypoints" vector, too:

var waypoint4 = [wp4,wp4alt,wp4dist,wp4angle,wp4length,wp4id, wp4brg];
var waypoint5 = [wp5,wp5alt,wp5dist,wp5angle,wp5length,wp5id, wp5brg];
var waypoints = [waypoint4, waypoint5]

The only problem with this approach is, that you'll need to set up all those wp variables - so there's a shorter version possible, by directly adding your data without using variables, i.e. "inline":

var waypoint4 = [1,1000,12,22,44,"none", 33];
var waypoint5 = [2,1500,22,42,14,"none", 133];
var waypoints = [waypoint4, waypoint5]

There's an even more succinct version possible. The next step would be to use embedded vectors directly:

 var waypoints = [[1,1000,12,22,44,"none", 33], [2,1500,22,42,14,"none", 133]]

Accessing such a vector with embedded (or nested) vectors is still simple:

 var waypoints = [[1,1000,12,22,44,"none", 33], [2,1500,22,42,14,"none", 133]]
 print(waypoints[0][0]) # prints 1
 print(waypoints[0][1]) # prints 1000
 print(waypoints[1][0]) # prints 2
 print(waypoints[1][1]) # prints 1500

The only issue here is that you'll need to keep vector ordering in mind, so that you can always access the right element number. This could be simplified by using some variables with telling names as the index for each vector:

 var NUMBER=0; var ALTITUDE=1; var DISTANCE=2; var ANGLE=3; var LENGTH=4; var ID=5; var BRG=6;
 var waypoints = [[1,1000,12,22,44,"none", 33], [2,1500,22,42,14,"none", 133]]
 print(waypoints[0][ALTITUDE]) # prints 1
 print(waypoints[0][DISTANCE]) # prints 1000
 print(waypoints[1][ALTITUDE]) # prints 2
 print(waypoints[1][DISTANCE]) # prints 1500

So, this would already be much better than our original version, because we can now have an arbitrary number of waypoints. New waypoints would need to be added to the waypoints vector by using the append() library function:

 var NUMBER=0; var ALTITUDE=1; var DISTANCE=2; var ANGLE=3; var LENGTH=4; var ID=5; var BRG=6;
 var waypoints = [[1,1000,12,22,44,"none", 33], [2,1500,22,42,14,"none", 133]]
 print(waypoints[0][ALTITUDE]) # prints 1
 print(waypoints[0][DISTANCE]) # prints 1000
 print(waypoints[1][ALTITUDE]) # prints 2
 print(waypoints[1][DISTANCE]) # prints 1500

 append(waypoints, [3,3000,122,212,34,"none", 133] );

Obviously, there's still one issue though: we are using lots of variables and helpers that all belong to the "waypoints" type, but which clutter our source code. So, next we are going to look into an even more flexible approach that nicely maps each waypoint field to a symbolic name, without having to remember vector indices.

A hash based version (recommended)

Now, to wrap these fields into a single variable that serves as the container for other variables, we could use a Nasal hash and start completely from scratch.

Consider the following empty hash:

var waypoint = {};

Next, we are going to add some fields to our new variable "container":

var waypoint = {number:0,altitude:0,distance:0,angle:0,length:0,ID:0,bearing:0};

This adds the following "member fields" to the waypoint hash and sets their initial value to 0:

  • number
  • altitude
  • distance
  • angle
  • length
  • ID
  • bearing

Now, to access any of these fields, we would use the "dot notation" by first specifying the name of the enclosing context (which is really just a fancy word for the name of the hash) and the name of the field we are interested in. You could read this as: LOCATION.FIELD (i.e. get FIELD out of location).

var waypoint = {number:0,altitude:0,distance:0,angle:0,length:0,ID:0,bearing:0};
print ( waypoint.number );
print ( waypoint.altitude );
print ( waypoint.distance );
print ( waypoint.angle );
print ( waypoint.length );
print ( waypoint.ID );
print ( waypoint.bearing );

So, the hash represents the surrounding environment (i.e. context or "namespace") in which these symbols are valid. For more information on namespaces, please see Namespaces and Methods.

Now, to make this is a little more interesting and to show what's happening, we are going to change the value of each field:

var waypoint = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};
print ( waypoint.number ); # prints 1
print ( waypoint.altitude ); # prints 2
print ( waypoint.distance ); # prints 3
print ( waypoint.angle ); # prints 4
print ( waypoint.length ); # 5
print ( waypoint.ID ); #6
print ( waypoint.bearing ); #7

So, you could obviously create several different versions of this hash to store your waypoint data:

var waypoint1 = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};
var waypoint2 = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};
var waypoint3 = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};
var waypoint4 = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};
var waypoint5 = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};

This is already a pretty cool thing, because you would end up with 5 different containers all having their own set of fields, that you can access and set arbitrarily - without affecting the other containers.

But the really cool thing comes next:

As you may have noticed, the code for each waypoint is 100% identical - so we could just as well tell the Nasal engine to use an existing hash as a TEMPLATE for a new object. This is accomplished using the "parents" keyword:

var Position3D = {x:1.00, y:2.00, z:3.00};
var p1 = { parents: [Position3D] };
var p2 = { parents: [Position3D] };
var p3 = { parents: [Position3D] };

This creates three different new containers/hashes by copying the fields from the hash specified in the parents vector.

What parents will do is this: whenever a hash contains a vector field named "parents:" pointing to other hashes, it will look up the parent hashes and use them as a template and copy the fields of the parent hashes to the new hash, i.e. less typing for you!

This means, that the previously posted code could be easily abbreviated and written like this:

var waypoint1 = {number:1,altitude:2,distance:3,angle:4,length:5,ID:6,bearing:7};
var waypoint2 = {parents:[waypoint1] };
var waypoint3 = {parents:[waypoint1] };
var waypoint4 = {parents:[waypoint1] };
var waypoint5 = {parents:[waypoint1] };

Once we start using a hash as a template for other hashes using the "parents" vector, we are actually creating a class that is copied to each new hash. This new copy of the class is called an "object" after creation.

A "class" is really just a template for a certain data type that consists of other data types and provides functions to work with the class. The functions publicly accessible are called its "interface" because these functions are meant to be used by the users of your class. This is in contrast to member fields which are usually not meant to be accessed directly.

Once a class is used as a template to create a new object, we say the class is getting "instantiated", i.e. an instance of the class (an actual object) is created. This makes it then possible to actually make use of its interface and access member functions (which are methods). Also see [1].


Now, given that the creation of new hashes using a template class is such a common thing to do - we could just as well add a new function to the parent hash that we can use to construct new hashes. As you could see already, the fields (or members) of a hash are specified in a well-defined form: "field_name:value".

var waypoint1 = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7
};

This isn't any different for fields that are of type "function":

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
hello: func {
 print("Hello");
}
};

Note that we have added a new field named "hello" to our waypoint hash. This can be easily accessed and also called:

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
hello: func {
 print("Hello");
}
};

var w = {parents: [waypoint]};
w.hello();

On the other hand, we were just about to make construction of such hashes much simpler. So we are going to change the function and make it return a new hash that is properly set up:

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
hello: func {
 return {parents:[waypoint]}
}
};

Note how the hello function has now been modified to return a new hash to its caller. So we could just change its name to something more telling like "new":

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
new: func {
 return {parents:[waypoint]}
}
};


So, whenever we need a new hash object of type "waypoint", we can simply call this construction function (which we'll call a constructor from now on):

var wp = waypoint.new();

Now, let's imagine you want to add another member function to print a certain field's value, such as "number". This can be accomplished using the "me" keyword which ensures that the member function is always referring to the active object:


var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
new: func {
 return {parents:[waypoint]};
},
show_number: func {
 print(me.number);
}
};

On the other hand, maybe you'd like to add some information (such as the number) during construction time to the object. So this would require changing the constructor function to accept a parameter, too:

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
new: func(n) {
 return {parents:[waypoint]};
},
show_number: func {
 print(me.number);
}
};

Note how the new function has been changed to accept a parameter named "n".

Now, to actually set the number field during construction time to the value of n, you could create a temporary hash:

new: func(n) {
 var t={parents:[waypoint]};
 t.number = n;
 return t;
}

Or in its entirety:

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
new: func(n) {
 var t={parents:[waypoint]};
 t.number = n;
 return t;
},
show_number: func {
 print(me.number);
}
};

Now, back to your initial example regarding a list of waypoints:


var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
new: func(n) {
 var t={parents:[waypoint]};
 t.number = n;
 return t;
},
};

var waypoints = [waypoint.new(), waypoint.new(), waypoint.new(), waypoint.new(), waypoint.new() ];

Note how this create a vector of 5 waypoints (0..4), each of these waypoints is a full object that can be conveniently accessed:

waypoints[0].bearing = 100;
waypoints[1].altitude = 100;
waypoints[2].ID = "none";

You can also use Nasal's support for looping to conveniently process these waypoints:

var waypoint = { 
number:1,
altitude:2,
distance:3,
angle:4,
length:5,
ID:6,
bearing:7,
new: func(n) {
 var t={parents:[waypoint]};
 t.number = n;
 return t;
},
dump: func {
 print("Altitude:", me.altitude, " distance:", me.distance, " Bearing:", me.bearing);
}
};

var waypoints = [waypoint.new(), waypoint.new(), waypoint.new(), waypoint.new(), waypoint.new(), ];

foreach(var w; waypoints) {
 w.dump();
}


This was a very simple introduction to object oriented programming in Nasal.