Object oriented programming in Nasal

From FlightGear wiki
Jump to navigation Jump to search


"Object Oriented Programming" is all about creating "things" (i.e. a cloud) with "actions" (transform, draw, update) (or "messages"). The main terminology includes:

Class
A description of a "type" that (typically) includes constructor (initial object setup), destructor (object cleanup), and various other methods. In Nasal, both classes and objects are a manifestation of the builtin hash type, and thus there is a loose distinction between the two: classes can be objects and vice-versa. A class acts basically as a template to create new objects with certain attributes (namely, certain variables (fields) and functions (methods)).
Object
Instance of a class, holds data specific to itself, as well referencing and inheriting from certain parent class(es). For example, when a class constructs an object, the object almost always has that class as its first parent.
Inheritance
When members/methods are visible/accessible in an object or class despite not explicitly belonging to them, by instead belonging to a "parent" (class) of the class/object and having the "child" inherit from that "parent" through the parents field. Such implicit members (fields/methods) are recursively looked up by traversing the parents vector.
Member / Field
A piece of data stored in an object (or class). Can be of any type, and it acts just like a local variable, though acts a little different in what to type to reference it (e.g. obj.foo or me.foo instead of just foo). Functions have a little different nomenclature, being called a method when called correctly:
Method
A member function of either the class or object that operates on a specific object using the me local variable to access that object.
Constructor
A function/method on the class itself that sets up and returns an instance of that class -- aka a ready-to-go object. Almost always called .new() in Nasal, and usually required for each class. (In Python simply "calling" the class will return a new object, and in C++ and JavaScript there is a "new" operator for this purpose, but neither of these work/are possible in Nasal.)
Destructor
A method on the object (not the class) to destroy it/take it down, including cleaning up references to other objects, particularly running loops, listeners and timers in FlightGear, but possibly any other persistent objects. Commonly called .del() in Nasal, and is generally not required except for more complex types with well-defined lifetimes.
me reference
Within a function: A handle to the object that contains the function as a member, used to access/handle/add instance-specific members and methods. There's no implicit symbolic resolution of members/methods so this "me" reference is required to get/set them. The code to access a member "foo" of the current object or of one of its parents would be me.foo.

Understanding hashes and namespaces

Please make sure to first read and understand Howto:Understand Namespaces and Methods.

Hashes as Containers for your variables and functions

Nasal, much like JavaScript, uses hashes with object notation to store values in containers, with keys for convenient access. A simple example is this:

# create a hash (container) and assign the name "storage" to it:
var storage = {};
# we can put values into "storage":
storage.value = 11;
# and we can take out values we put into it
print(storage.value); # will print 11 because we set "storage.value" to 11 last and it hasn't been modified since

It should be noted that both of these styles are perfectly valid:

var obj = {
    foo: 9,
    "bar": 10,
};
obj.foo;
obj["foo"];
obj.bar;
obj["bar"];

But be warned that there are a few differences between those two methods (identifiers versus strings), and in particular only the "dot" syntax (e.g. obj.foo) will work with inheritance (which is used in both objects and classes). In general it is best to be consistent and only use one or another with an object. If it's declared using identifiers, use dot syntax. If it's declared using strings, use bracket-and-string syntax. The convention is to use strings for non-OOP values - simple hashes acting as data (sort of like structs in C/C++, but more like associative containers in other languages, say Python, where the two notations do very different things).

Basic introduction and Examples

A class (or hash in Nasal) is the "template" for a "thing" containing a number of member attributes/fields (variables and functions). These are inherited by instances of the class, and the class' functions that are called on the object have access to the latter's attributes.

Conventionally, you would have a number of functions working with global state (variables), but a class/instance relationship (OOP) allows you to wrap all such shared state in dedicated objects, so that related state is kept in the same place, imagine it like a container for your variables and functions.

So the class only describes the "layout" or the properties and methods of objects that can be created. To actually use a class, it has to be "instantiated" (constructed/made) which means creating an object using a specific "template class" (or even several different classes with multiple inheritance).

These member fields can be variables (e.g. lat, lon, alt) or functions (e.g. setAlt() or setPos()).

And the actual instance (say, cloud[n] in the property tree) of such a thing is then called an "object".

Basically, the point is that you have "containers" called "classes" which can be used to create new objects, called "instances". Once a new object is created, it inherits all the attributes from the parent class, which is used as a "template". This means that you can declare a simple template class with certain variables and functions, and each "version" ("instance") of your class will have these variables and functions.

Simple Class Template

Here's a minimal example which will fit most simple classes (replace names as appropriate):

var Cls = {
    new: func(arg...) {
         var m = { parents:[Cls] };
         m.foo = (size(arg) > 0) ? arg[0] : 9;
         return m;
    },
    get_foo: func() { return me.foo },
    set_foo: func(foo) { me.foo = foo },
    exec: func() { print(me.foo) }
};

The constructor, Cls.new() consists of three simple steps:

  • Create a new object, referenced as "m", that inherits from this class, and thus has all of the members that Cls has: new (which is generally called only from the class), get_foo, set_foo, and exec.
  • Add a member "foo" to the object, with the value passed to new, or 9 if new was called without any arguments. Note that this member is also what the other methods deal with, like the interface pair of get_/set_foo, and the print routine of exec. However, any other code is also able to reference that member too - unlike C++ and other languages' "private" members, these are strictly public.
  • Finally, we have to return the object. Since Cls.new() is just a function (i.e. there's no special case for it), it has to both create and return the object, which can be a foreign concept to those coming from other languages with object-oriented capabilities.

For more specifics on these topics, see the appendix.

Methods are Functions in a Class

Functions in a class are called "methods" because they work with instance-specific variables. Now, you can just as easily add methods (=functions) to your new position3D class template:

var position3D = {
    x:100.00,
    y:200.00,
    z:300.00,
    hello: func() {
        print("Hello world"); 
    }
};

This add a new method (=function) to the hash called "hello", which takes no arguments (empty argument list).

As you can see, you can easily call a method like this:

var test = {parents:[position3D] };
test.hello();

Now, the interesting part is accessing object-specific (instance) variables, i.e. variables that are specific to the "version" of the class you created. This is accomplished by prepending the keyword "me." to your class variables, so that the interpreter knows that you want to access the instance data:

var position3D = {
    x:100.00,
    y:200.00,
    z:300.00,
    info: func {
        print("x:",me.x, " y:", me.y, " z:", me.z);
    },
};

Functions that work with instance specific state are called "methods", they may refer to instance specific state using a "self reference" (me) in Nasal, that ensures that access to a field or method is using the right instance-specific data.

Creating Objects from Class templates: Instantiation

var position3D = {
    x:100.00,
    y:200.00,
    z:300.00,
};

This declares a new "hash" named "position3D", now to use it as a class template, you would just need to create a new hash and declare the "parents" vector to point to the class "position3D":

var test = {parents:[position3D] };

This tells the Nasal engine:

  • create a new variable named "test"
  • make it a hash
  • set the parents vector to point to "position3D"
  • i.e. use position3D as a "template"
  • and inherit all the fields/functions from the position3D, so that the new "test" variable has them too

Basically, this will give you an exact copy of the original position3D class, without having to specify it fully (i.e. less typing for you!).

Now, when accessing the new "test" variable, you'll see that it has inherited the fields from "position3D":

var test = {parents:[position3D] };
print(test.x);
print(test.y);

Composition: Classes containing other Classes

Classes may be composed of other classes, i.e. a "cloud field" class would in turn contain "cloud" classes. This is then called "composition".

Another way is inheritance, which is really just like instantiation but with an option to overload/parametrize the original class, where a new type may inherit properties (fields:variables and methods) from a "parent" class:

var position3D = {x:0,y:0,z:0};
var new_class = { parents:[position3D] };

Imagine it like taking a "template" for the class and then saying "make a new class using this template", to ensure that the new class has certain fields and methods (x,y,z in this case). This could for example be the case for a vehicle class, which may have properties to define the maximum groundspeed.

Inheritance has the added advantage of providing a means to customize class behavior without having to modify the original class, because all member fields can be parametrized in your new templated class.

For example, a "cumulus" cloud class could be derived from the "cloud" class, just by parametrizing it (different cloud model, different texture, different transformations), without touching anything in the actual "cloud" class. You would just be creating a copy of the original class, using it as a template and then override anything that you want to be customized.

This is basically how OOP may be understood: things are classified according to "is a" or "has a" relationship.

Encapsulation: Hiding implementation details

In OOP, internal state is managed by wrapping everything in a class using accessor functions for modifying and getting internal values. So internal state would in turn only be modified by an abstract interface: class "methods", instead of directly accessing class-internal fields.

This provides a way for managing access to a member variable (field), such an abstract interface is also useful for keeping logic private, and internal. For example, the name of a variable "altitude" can be easily changed internally to "altitude_ft", without having to rename all users of the class - simply because all other code will refer to the methods providing access to the field, such as setAltitude() or getAltitude()

For example, instead of doing something like:

cloud.lat=10.22; 
cloud.lon=43.22;

...you would have a method accepting lat/lon variables:

cloud.setPos(lat, lon);

That means that the actual variables containing the values for lat/lon are not exposed or used outside the actual object itself. This is called "encapsulation" and provides you with a way to manage state and ensure that internal state is valid at all times, because other code may only use the external interface, which may contain additional logic to verify/validate all attempts at changing internal state

This allows you for example to simply rename a class variable, without having to change any of the code that uses the object, because other code only uses public class methods.

Another important thing in OOP is separation of concerns, i.e. you don't want to end up with huge bloated "super classes" that manage all sorts of different state, but instead use different classes where appropriate, to split code into abstract "modules" with well-defined responsibilities.

So, one of the very first steps to convert procedural code to OOP code would be to group your code into a number of logical "classes" (e.g. cloud, cloud field, position3D ).

Of course, one may still use objects like conventional variables for passing and returning parameters, in other words: Your functions can also accept objects as arguments and return new objects. A simple function which returns new objects, may be an allocator to create new objects:

var position3D = {x:0,y:0,z:0};
var new_position = func {
  return { parents:[position3D] };
}

A more detailed tutorial with code examples is available here Howto: Start using vectors and hashes in Nasal (see the hash section).

Apendix: more specifics

Nasal does not have the concept of "classes", understanding by a class an object definition that cannot exist as an instance on its own. What we always have in Nasal is hashes that perhaps reference other hashes via the parents vector.

The "Parents" Vector

As seen throughout this tutorial, hashes can inherit from other hashes using the parents field, which contains a vector of those other hashes. Typically this is used to implement class versus object distinction, but this is necessarily an arbitrary distinction. It can also be used to make one object inherit from another - like a "dual citizenship" - or even class inheritance: one class is a complete subtype of its "superclass" or maybe an implementation of an "[abstract] base class". Other languages, particularly C++, have more well-defined notions of these concepts, but Nasal can (almost) always emulate them.

The parents vector is just a symbol lookup trick. The only thing it does is to add other hashes as targets where Nasal can search for symbols that are not defined in the current hash. This serves for implementing object inheritance in a traditional way as long as:

  1. The symbols that you expect to be gathering from the parented hashes are functions. This is because when you are executing a function in a parent hash, the me symbol will allow you you to resolve the context back to the instance hash down the hierarchy.
  2. Non function members that are expected to be kept per instance, are defined in the instance hash. Otherwise those members might end up being shared across derived hashes due to the parents lookup mechanism.
  3. Members that are expected to be shared across different instances are defined in the template hash and not in the instances themselves. Which means that you have to be careful on how you write values to them or you will end up creating a new member in the instance hash that will shadow the shared member. (Mmm, this one is a dangerous pitfall, but I remember seeing this explained in the wiki, and is not that often that you need this...).

The "me" reference

WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.

The me reference is a syntactic trick, and it can be dangerous to use depending on the usage pattern, so some good practices are in order, in particular

1. When writing a constructor in a template object, do not use me in the parents vector, because me will resolve to a hash reference only when the new() method is called, and which hash will it be depends on how the method is called.

2. When calling a "super" class method from an overriding method, use the Nasal call() library function, specifying the instance hash as the me parameter. If you use the parents vector directly, me will likely end up referring to a template hash instead and you will loose the instance context. Basically you have an expression on the left side of a dot, and then a lvalue and parenthesis on the right side -- no exceptions! The expression is evaluated once, with the value being DUP'ed (duplicated/copied) and used both for the member get (i.e. what is the function contained in the hash) and as a special argument to the function (now a method) call. That "me" is then set as a symbol like an argument, originating from the left-hand expression, and is otherwise unset.

In other cases, using the call() builtin, it is possible to specify the me reference manually, but what "me" is, is otherwise a consequence of how it is called. If the syntax differs, there is no me local variable. (One common example is passing a MEMBER of an object to a function as a parameter or saving it to a variable - i.e. then it doesn't matter if it is a function or not.) (I use MEMBER here to distinguish from METHODS, where a METHOD uses "me" and a MEMBER doesn't.)

The thing with a func {} expression is that it delays evaluation, so it can keep the proper syntax for the method call and therefore pass the correct me reference, which is preserved in its closure.

Parents inheritance is really simple: to find a member you search for it in the current hash or try finding it in each parent (indexes 0..n), throwing an error if there is too much recursion. Basically, inheritance is implemented such that there's a mutable parents vector that maintains a list of hashes that are used for recursive symbol lookups, me will be defined per instance and point to the outer hash. Which is why shadowing can occur.

me is also mutable, i.e. if it isn't defined, it can be explicitly defined, or passed via call() - a mehod which we tend to use in MapStructure whenever we want to ensure that the proper instance is passed, i.e. no variable shadowing taking place.

  • FUNC() -> no me
  • OBJ.METHOD() -> me = OBJ
  • (OBJ.MEMBER_FUNC)() -> no me
  • (EXPR).METHOD() -> me = (EXPR)


Implementation Management

As alluded to before, Nasal can have abstract-base-class-ish things, where a true implementation of the interface is left to specific types of that ABC. For example, in MapStructure, the Symbol is mostly an ABC, while specific types of it (e.g. those implementing VORs or TFC or DMEs) do the real action. It is convenient to have the class itself manage these instances, and have a "registration" of all sub-types. The rough code for it looks like this:

var ABCls = {
    registry: {}, # hash of name_of_type to implementation_class
    # Register a new type of implementation
    add: func(type, class)
        me.registry[type] = class,
    # Get a previously-added type
    get: func(type)
        if ((var class = me.registry[type]) == nil)
            die("unknown type '"~type~"'");
        else return class,
    # Call corresponding constructor, i.e. the first argument is
    # reserved for the type's name, the others are passed on to
    # its constructor
    new: func(type, arg...) {
         # Get the class's new method, call it with arg=arg and me=(the class)
         # all in one line:
         var m = call((var class = me.get(type)).new, arg, class);
         # Could be rewritten as:
         #   var class = me.get(type);
         #   var m = call(class.new, arg, class);
         return m;
    },
};

Note how the .new() is simply a proxy (i.e. facade) to the implementation's constructor. It does not implement any type of constructor - that is left up to the implementation. However, if needed, there are ways to implement it so that .new() would act as both a proxy and a regular constructor.

Almost-Static Members

The general drift is that Nasal doesn't have a well-defined structure for classes, unlike C++, for it can emulate certain behaviors of those, like constructors, inheritance, multiple inheritance, etc. etc. This is true for "static" members as well. In languages with such a thing, a static member doesn't belong to one instance, but rather to all of them as a whole (or to the class, or to an outer scope - it doesn't matter, as long as it is thought of as "shared" everywhere). In Nasal, this would be emulated with a member of a class. As a simple example:

# Create a class
var Cls = {
    # Declare a pseudo-static member
    static: 9,
    # Simple constructor
    new: func return { parents:[Cls] },
    # Set the static member
    set_static: func(static) Cls.static = static,
    # Get the static member
    get_static: func Cls.static,
    # Print the static member
    print_static: func print(Cls.static),
};

# Create an instance:
var Inst = Cls.new();
# Now, it doesn't matter whether we use Inst or Cls
# for our left side, each method will work the same:
Cls.print_static(); # prints 9, so will: Inst.print_static()
Cls.get_static() * 9; # 81
Inst.set_static(88);
Cls.print_static(); # prints 88 now
var Inst2 = Cls.new();
Inst.print_static(); # this one does too
Inst2.print_static(); # as does this one, so we can conclude it is shared between all instances *and* the class itself

Given the success with using the thin API (the *_static() methods), one might conclude that both of the following might work equally well, but they don't:

Inst.static = 81;
Cls.static = 91;

One slightly weird thing about Nasal is that setting a member (i.e. anything of the form obj.foo = nil;) sets it in the instance, never in any parent. The result of the above statement is that now we have two members called static: one in Inst (whose value is 81) that which obscures the other one, the one in Cls (whose value is 91). The one in Cls is now accessible via Cls.static or Inst2.static, but not Inst.static. However, the other weird thing is that the members previously defined (*_static()), still work on Cls - but that shouldn't be surprising, because they were written referencing "Cls" and not "me", do you see now why that is by design? It ensures that they still have the correct behavior, no matter how they are called.

The other thing to address is the opposite issue: mutable members defined in a class. (TODO)

Making safer base-class calls

There's a lot of Nasal code floating around doing the equivalent of this when using multiple inheritance to make base class calls:

object.parents[x].method();

Internally, this works such that Nasal's inheritance mechanism relies on a so called parents vector that contains a list of classes that are used for field/method look-ups. This vector can be accessed using numeric indexes - thus, the correct value for x is directly affected by the way the parents vector is set up, i.e. its internal class ordering:

var myClass = {parents:[FirstBaseClass, SecondBaseClass, ThirdBaseClass] };

However, this practice of using indexed parents access to look up a base class should be considered a "hack" and discouraged, because it's not a particularly robust or even safe way to call a superclass, simply because it doesn't tell Nasal anything about the name of the class you're trying to call, but merely its index in the inheritance/lookup vector, which may be subject to change (e.g. due to refactoring). Thus, we encourage people to actually use Nasal's built-in call() API to accomplish the same thing in a more robust fashion:

call(Class.method, [], me);

This will tell Nasal that you want it to call a certain method in a particular class - and if that fails, you'll get better diagnostics (error messages), too. The main problem here is that the other approach is pretty vulnerable when restructuring your code, as it is heavily reliant on inheritance ordering - which is something that isn't exactly straightforward: code shouldn't "break" just because the inheritance ordering is modified. Thus, please try to use the latter idiom. If in doubt, just get in touch via the Nasal sub forum.

To pass argument to the method, just add them to the 2nd argument, i.e. the empty vector:

call(Class.method, [nil,nil,nil], me);

To pass a custom namespace environment, you can use this:

var namespace = {};
call(Class.method, [nil,nil,nil], me, namespace);

Which would be equivalent to this example using an anonymous namespace:

call(Class.method, [nil,nil,nil], me, {} );

(If you want to preserve/modify the namespace, it makes sense not use an anonymous namespace though).

To do exception handling, you can pass an empty vector and check its size (>=1):

var err = [];
call(Class.method, [nil,nil,nil], me, {}, err );
if(size(errors))
print("There was some problem calling a base class method: Class.method()");

You can also declare the variable expression inline:

call(Class.method, [nil,nil,nil], me, var ns={}, var err=[] );
if(size(errors))
print("There was some problem calling a base class method: Class.method()");
else {
print("Success, namespace is:");
debug.dump(ns);
}