Nasal scripting language: Difference between revisions

Jump to navigation Jump to search
Line 731: Line 731:


[[Nasal_scripting_language#settimer.28.29|More information about the settimer function is below]]
[[Nasal_scripting_language#settimer.28.29|More information about the settimer function is below]]
== OOP - Object Oriented Programming ==
In Nasal, objects ("classes") are regular hashes. Self-reference and inheritance are implemented through special variables <tt>me</tt> and <tt>parents</tt>. To get a better understanding of the concept, let's start with the very basics. A detailed step by step introduction is available here: [[Howto: Start using vectors and hashes in Nasal]]. A more high level description of the ideas behind OOP is here: [[Object oriented programming in Nasal]].
=== Hashes ===
Hashes, also known as "dictionaries" in Python or "maps" in C++/STL are data structures that hold key/value pairs in a way that allows quick access to a value via its key.
var airport = {
    "LOXZ": "Zeltweg",
    "LOWI": "Innsbruck",
    "LOXL": "Linz Hoersching",    # the last comma is optional
};
print(airport["LOXZ"]);            # prints "Zeltweg"
airport["LOXA"] = "Aigen";        # adds LOXA to the hash
The built-in function keys() returns a vector with the keys of the hash.  The function values() returns a vector with the values of the hash. For example:
  debug.dump (keys(airport)); #prints ['LOXZ', 'LOWI', 'LOXL']
  debug.dump (values (airport)); #prints ['Zeltweg', 'Innsbruck', 'Linz Hoersching']
The quotes around keys can be left away in a hash definition if the key is a valid variable name or a number. This works just as well:
var airport = {
    LOXZ: "Zeltweg",
};
There's also an alternative way to access hash members if the keys are valid variable names: <tt>airport.LOXI</tt> can be used instead of <tt>airport["LOXI"]</tt>. There is a difference, though, which is described in the next section.
Note that assigning a hash (or a vector) to another variable does never ''copy'' the contents. It only creates another reference to the same data structure. So manipulating the hash via its new name does in fact change the one, original hash.
var a = airport;
a.LOXL = "Linz";          # same as airport.LOXL!
print(airport.LOXL);      # prints now "Linz", not "Linz Hoersching"
(True copies of vectors can be made by assigning a full slice: <tt>var copy = vec[:]</tt>. There's no such method for hashes.)
=== What are classes? ===
A class is a simple way to come up with a new data type (think "aircraft", "house", "vehicle", "tree") that may consist of an arbitrary number of other types:
* position: latitude, longitude
* altitude
* orientation
and also functions, such as:
* setPosition(lat,lon)
* setOrientation(heading)
* setAltitude()
Rather than implementing functions separately and passing the instance operands as arguments, the functions are implemented next to the data, and have an implicit operand pointing to the active instance of the object, so that all fields and functions belonging to each other, are implemented/wrapped inside the same enclosing context (hash/class).
Using inheritance, it is easy to create new instances of such a class that automatically shares all properties of the parent class, including member fields and member functions (so called methods).
So, rather than calling:
<syntaxhighlight lang="php">
setAltitude(house, 1000);
</syntaxhighlight>
You would end up with a "house" object that implicitly contains a method named "setAltitude" that can be called:
<syntaxhighlight lang="php">
house.setAltitude( 1000 );
</syntaxhighlight>
=== Self-reference: "<tt>me</tt>" ===
Values stored in a hash can be of any type, even of type ''function''. Member functions ("methods") can reference their own enclosing hash via reserved keyword <tt>me</tt>. This is comparable to the <tt>this</tt> keyword in C++ classes, or the <tt>self</tt> keyword in Python.
var value = "test";
var data = {
    value: 23,                        # scalar member variable
    write1: func { print(value); },    # function member
    write2: func { print(me.value); }, # function member
};
data.write1();    # prints "test"
data.write2();    # prints 23
The above example is already a simple form of an object. It has its own variable namespace (''data''), its own methods, and it can be passed around by-reference as one unit. Such classes are sometimes called ''singleton classes'', as they are unique, with no independent class instances. They mostly serve as a way to keep data and methods nicely encapsulated within a Nasal module. Often they contain a method for initializing, which is usually called <tt>init</tt>.
=== Inheritance: "<tt>parents</tt>" ===
What we learned about <tt>me</tt> in the last section is only half the truth. "me" doesn't only reference an object's own hash, but also one or more parent hashes. <tt>parents</tt> is another reserved keyword. It denotes a vector referencing other object hashes, which are "inherited" that way.
Please note that Nasal's currently supported form of encapsulation does not provide support for any form of data/information hiding (restricting access), i.e. all hash fields (but also all hash methods) are always publicly accessible (so there's nothing like the "private" or "protected" keywords in C++: in this sense, Nasal's inheritance mechanism can be thought of like C++ structs which are also public by default).
The major difference being, that all members (functions and fields) are also always '''mutable''', which means that functions can modify the behavior of other functions quite easily, this also applies to the parents vector, too.
var parent_object = {
    value: 123,
};
var object = {
    parents: [parent_object],
    write: func { print(me.value) },
};
object.write();    # prints 123
Even though <tt>object</tt> itself doesn't contain a member <tt>value</tt>, it finds and uses the one of its parent object. <tt>parents</tt> is a vector that can contain several parent objects. These are then searched in the order from left to right, until a matching member variable or method is found. Each of the parents can itself have parents, which are all recursively searched.
In the section about hashes it was said that hash members can be accessed in two alternative ways, and that's also true for methods. <tt>object.write()</tt> could also be called as <tt>object["write"]()</tt>. But only in the first form will members also be searched in parent hashes if not found in the base hash, whereas the second form creates an error if it's not a direct member.
=== Creating class instances ===
With <tt>me</tt> and <tt>parents</tt> we can implement a class object and create independent instances from that:
var Class = {
    write:    func { print(me.value); },
    increment: func { me.value += 1; },
};
var instance1 = { parents: [Class], value: 123 };
var instance2 = { parents: [Class], value: 456 };
instance1.write();    # prints 123
instance2.write();    # prints 456
As you can see, the two class instances are separate, independent objects, which share another object as parent -- they "inherit" from object <tt>Class</tt>. One can now easily change members of any of these three objects. The following will redefine the parent's <tt>write</tt> method, and all instances will automatically use this new version:
Class.write = func { print("VALUE = " ~ me.value) }
But one can also add a method to just one instance:
instance1.write = func { print("VALUE = " ~ me.value) }
Because <tt>instance1</tt> does now have its own <tt>write</tt> method, the parents won't be searched for one, so <tt>Class.write</tt> is now overridden by <tt>instance1</tt>'s own method. Nothing changed for <tt>instance2</tt> -- it will still only find and use <tt>Class.write</tt> via its parent.
Note, the we couldn't create a class instance by simple assignment, because, as we learned above, this wouldn't create a separate copy of the Class object. All "instances" would reference the same hash!
var bad_instance1 = Class;  # bad
var bad_instance2 = Class;  # bad
bad_instance1.value = 123;  # sets Class.value to 123
bad_instance2.value = 456;  # sets Class.value to 456
bad_instance1.write();      # prints 456, not 123
=== Constructor ===
Defining each class instance by explicitly creating a hash with parents is clumsy. It is nicer to have a function that does that for us. Then we can also use function arguments to initialize members of this instance.
var new_class = func(val) {
    return { parents: [Class], value: val };
}
var instance1 = new_class(123);
var instance2 = new_class(456);
instance1.write();  # prints 123
instance2.write();  # prints 456
Because the class generating function <tt>new_class()</tt> really belongs to class <tt>Class</tt>, it would be nicer to put it into the class hash as well. In this case we call it a class "constructor", and as a convention, give it the name ''new''. It could have any name, though, and there could be more than one constructor.
var Class = {
    new: func(val) {
        return { parents: [Class], value: val };
    },
    write: func {
        print("VALUE=" ~ me.value);
    },
};
var instance1 = Class.new(123);
var instance2 = Class.new(456);
As you can see, <tt>new()</tt> doesn't return a copy of <tt>Class</tt>, but rather a small hash that contains only a list of parents and one individual member <tt>value</tt>.
Classes aren't always as simple as in our example. Usually they contain several members, of which some may have yet to be calculated in the constructor. In that case it's easier to create a local object hash first, and to let the constructor finally return it. Such local hashes are often named <tt>m</tt> (as a short reference to <tt>me</tt>), or <tt>obj</tt>.
var Class = {
    new: func(val) {
        var m = { parents: [Class] };
        m.value = val;
        return m;
    },
    write: func {
        print("VALUE=" ~ me.value);
    },
};
This last example is the most frequently used form of class definitions in FlightGear-Nasal.
=== Destructor ===
There's no such thing in Nasal. In other languages destructors are automatically called when the class gets destroyed, so that memory and other resources that were allocated by the constructor can be freed. In Nasal that's all done by the Garbage Collector (GC), anyway. In the FlightGear context, however, there ''are'' resources that should get freed. Listeners should get removed, self-calling functions ("loops") stopped. For that it's recommended to create a destructor function and to call that manually. Such functions are often called <tt>del</tt>, similar to Python and to pair nicely with the three-letter constructor name <tt>new</tt>.
=== Memory management ===
Finally, as you know now, Nasal, being a dynamic programming language, doesn't require or support any manual memory management, so unlike C++, you don't need to call operators like "new" or "delete" to allocate or free your memory.
However, if you do know that you don't need a certain variable anymore, you can certainly give a hint to the built-in garbage collector to free it, by assigning a "nil" value to it.
This can certainly pay off when using more complex data structures such as nested vectors or hashes, because it will tell the built-in garbage collector to remove all references to the corresponding symbols, so that they can be freed.
It is also possible to make use of Nasal's delete() function to remove a symbol from a namespace (hash).
So, if you are concerned about your script's memory requirements, using a combination of setting symbols to nil, or deleting them as appropriate, would allow you to create helper functions for freeing data structures easily.
In addition, it is probably worth noting that this is also the only way to sanely reset an active Nasal namespace or even the whole interpreter. You need to do this in order to reload or re-initialize your code without restarting the whole FlightGear session [[Nasal_scripting_language#Managing_timers_and_listeners]].
Obviously, you should first of all ensure that there is no more code running, this includes any registered listeners or timers, but also any others loops or recursive functions.
Thus, if you'd like to reload a Nasal source file at run time, you should disable all running code, and then reset the corresponding namespace, too. This is to ensure that you get a clean and consistent namespace.
Nasal provides a number of core library functions to manipulate namespaces, such as:
* caller() - to get a strack trace of active functions currently on the Nasal stack
* compile() - to compile new Nasal code "on the fly", i.e. dynamically from a string
* closure() - to query the lexical namespace of active functions
* bind() - to create new function objects
More information is available here: http://www.plausible.org/nasal/lib.html
If, on the other hand, you are using these data structures in some repeated fashion, it might make sense to keep the data structure itself around and simply re-use it next time (overwriting data as required), instead of always allocating/creating a new one, this is called "caching" and can pay off from a performance perspective.
=== Multiple inheritance ===
A class can inherit from one or more other classes. It can then access all methods and class members of all parent classes, but also override them and add additional members.
var A = {                                            # simple class A
    new: func {
        return { parents: [A] };
    },
    alpha: func print("\tALPHA"),
    test:  func print("\tthis is A.test"),
};
var B = {                                            # simple class B
    new: func(v) {                                  # ... whose constructor takes an argument
        return { parents: [B], value: v };
    },
    bravo: func print("\tBRAVO"),
    test:  func print("\tthis is B.test"),
    write: func print("\tmy value is: ", me.value),
},
var C = {                                            # class C that inherits ...
    new: func(v) {
        return { parents: [C, A.new(), B.new(v)] };  # ... from class A and B
    },
    charlie: func print("\tCHARLIE"),
    test:    func print("\tthis is C.test"),        # overrides A.test() and B.test()
};
print("A instance");
var a = A.new();
a.alpha();
print("B instance");
var b = B.new(123);
b.bravo();
b.write();
print("C instance");
var c = C.new(456);
c.alpha();                        # use alpha from the A parent
c.bravo();                        # use bravo from the B parent
c.charlie();                      # use charlie from C itself
c.test();                        # use C.test(), which overrides A.test() and B.test()
c.write();
Even if a class overrides a method of a parent with the same name, the parent's version can still be accessed via <tt>parents</tt> vector.
c.test()              # use C.test()
c.parents[0].test();  # use C.test()
c.parents[1].test();  # use A.test()
c.parents[2].test();  # use B.test()
=== More on methods: Chaining ===
Methods are function members of a class hash. They can access other class members via the <tt>me</tt> variable, which is a reference to the class hash. For this reason, a method returning <tt>me</tt> can be used like the class itself, and one can apply further methods to the return value (this is usually called "method chaining"):
var Object = {
    new: func(coords...) {
        return { parents: [Object], coords: coords };
    },
    rotate: func(angle) {
        # do the rotation
        return me;
    },
    scale: func(factor) {
        # do the scaling
        return me;
    },
    translate: func(x, y) {
        # do the translation
        return me;
    },
};
var triangle = Object.new([0, 0], [10, 0], [5, 7]);
triangle.translate(-9, -4).scale(5).rotate(33).translate(9, 4);    # concatenated methods thanks to "me"
=== More on methods: Methods as Listener Callbacks ===
<tt>me</tt>, however, is only known in the scope of the class. If a method is to be called as a listener callback or a timer function, <tt>me</tt> has to get wrapped in a function, so that it's stored in the function closure.
var Manager = {
    new: func {
        return { parents: [Manager] };
    },
    start_timers: func { 
        settimer(do_stuff, 5);            # BAD: there's no "do_stuff" function in the scope
        settimer(me.do_stuff, 5);        # BAD: function exists, but "me" won't be known
                                          #      when the timer function is actually executed
        settimer(func me.do_stuff(), 5);  # GOOD: new function object packs "me" in the closure
        setlistener("/sim/foo", func me.do_stuff());  # GOOD  (same as with timers)
    },       
    do_stuff: func {
        print("doing stuff");
    },
};
var manager = Manager.new();
manager.start_timers();
'''See [[Namespaces and Methods#Methods]] for more information.'''


== Exception handling ==
== Exception handling ==

Navigation menu