Nasal Conditionals

From FlightGear wiki
Jump to navigation Jump to search


Sometimes you may only want to execute certain portions of your code, depending on whether various conditions are met. This can be accomplished in several ways, one of the most straightforward ways being an if expression, which has the form:

var condition = 1; # this evaluates to boolean true
if (condition) {
    # true block, will  be executed if condition is true
} else {
    # false block, will be executed if condition is false
}

If you wish to execute code only if a condition is not true, use the ! operator:

var condition = 1; # boolean true
if (!condition) {
    # false block, will  be executed if condition is true
} else {
    # true block, will be executed if condition is false
}

In both examples, the condition variable is just a placeholder, it could be a variable named condition, but it could also be a more complex expression, such as a getprop call for example:

var condition = getprop("/sim/signals/freeze");

if (condition) {
    # true block, will  be executed if condition is true i.e. FlightGear is paused
} else {
    # false block, will be executed if condition is false
}

The same thing could be accomplished by using a call to getprop() as part of the condition:


if (var condition = getprop("/sim/signals/freeze")) {
    # true block, will  be executed if condition is true
} else {
    # false block, will be executed if condition is false
}

Note that you are never using the condition variable here, so you could also omit the whole declaration and directly use the getprop call:


if (getprop("/sim/signals/freeze")) {
    # true block, will  be executed if condition is true
} else {
    # false block, will be executed if condition is false
}

The condition can also be arbitrarily complex, in that it can be made up of other conditions, that may be nested using these boolean operators:

  • ! (not)
  • and
  • or
var condition = (1 == 1 and 2 == 2 and 3 == 3) or (4 != 5 and !0);
if (condition) {
    # true block, will  be executed if condition is true
} else {
    # false block, will be executed if condition is false
}


Conditionals

Nasal has no "statements", which means that any expression can appear in any context. This means that you can use an if/else clause to do what the ?: (ternary operator) does in C. The last semicolon in a code block is optional, to make this prettier. First, a completely conventional implementation of the abs function:

var abs = func(n) { 
    if(n<0) 
    { return -n }
    else 
    { return n }
}

This form would work as it is in "C" and, with minor adjustments for syntax, in many other languages. However, the following form would not work in "C":

var abs = func(n) { 
    return if (n<0) 
    { -n }
    else 
    { n }
}

In this form the if clause is clearly an expression that yields a value. That value is the result of the last expression executed within the if clause.

And for those who don't like typing, the ternary operator works like you expect:

var abs = func(n) { n < 0 ? -n : n }

This also illustrates that a function will return a value even when there is no return clause. The value returned is the value of the last expression evaluated within the function.

In addition, Nasal supports braceless blocks, like they're known from C/C++ and other languages. Basically, the block ends with a semicolon, instead of being encapsulated inside a {/} pair. This means basically that the first statement (expression) after a conditional is evaluated if the condition is true, otherwise it will be ignored:

var foo=1;
if (foo)
    print("1\n");
else
    print("0\n");
print("this is printed regardless\n")


Instead of a switch statement one can use

if (1==2) {
    print("wrong");
} else if (1 == 3) { # NOTE the space between else and if
    print("wronger");
} else {
    print("don't know");
}

which produces the expected output of don't know.

Instead of "else if", you can also use the shorter equivalent form "elsif":

if (1 == 2) {
    print("wrong");
} elsif (1 == 3) { 
    print("wronger");
} else {
   print("don't know");
}


The nil logic is actually quite logical, let's just restate the obvious:

if (nil) {
    print("This should never be printed");
} else {
    print("This will be printed, because nil is always false");		
};


Nasal's binary boolean operators are "and" and "or", unlike C. Unary not is still "!" however. They short-circuit like you expect:

var toggle = 0;
var a = nil;
if (a != nil and a.field == 42) {
    toggle = !toggle; # doesn't crash when a is nil
}

Note that you should always surround multiple conditions within outer parentheses:

if (1) do_something();
if (1 and 1) do_something();
if ( (1) and (1) ) do_something();
if ( (1) or (0) ) do_something();

Defaulting

Nasal's binary boolean operators can also be used to default expressions to certain values:

var altitude = getprop("/position/altitude-ft") or 0.00;

Basically this will first execute the getprop() call and if it doesn't return "true" (i.e. if it isn't zero or nil), it will instead use 0.00 as the value for the altitude variable.

Regarding boolean operators like and/or: This is working because they "short circuit", i.e. in an OR comparison, the remaining checks are NEVER done IF any single of the previous checks is true (1), because that'll suffice to satisfy the OR expression (i.e. knowing that a single check evaluates to true).

Similarly, an "and" expression will inevitably need to do ALL "and" checks in order to evaluate the whole expression.

At first, this may seem pretty complex and counter-intuitive - but it's important to keep this technique in mind, especially for variables that are later on used in calcuations:

var x = getprop("/velocities/airspeed-kts") or 0.00; # to ensure that x is always valid number
var foo = 10*3.1*x; # ... for this computation

So this is to ensure that invalid property tree state does not mess up the calculations done in Nasal by defaulting x to 0.00, i.e. a valid numeric value.

Bottom line being, using "or" to default a variable to a given value is a short and succinct way to avoid otherwise elaborate checks, e.g. compare:

var altitude_ft = getprop("/position/altitude-ft");
if (altitude_ft == nil) {
    altitude_ft = 0.0;
}

versus:

var altitude_ft = getprop("/position/altitude-ft") or 0.00;

Basically: whenever you read important state from the property tree that you'll use in calculations, it's a good practice to default the value to some sane value.

But this technique isn't really specific to getprop (or any other library call).

Nasal is a language that has no statements, it's all about EXPRESSIONS. So you can put things in pretty much arbitrary places.

var do_something = func return 0;
if (a==1) {
    do_something() or print("failed");
}

var do_something = func return 0;
( (a==1) and do_something() ) or print("failed");

In a boolean comparison, 1 always means "true" while 0 always means "false". For instance, ANY comparison with if x ==1 can be abbreviated to if (x), because the ==1 check is implictly done:

var do_something = func return 0;
( a and do_something() ) or print("failed");

Using Hashs to map keys to functions

You can easily reduce the complexity of huge conditional (IF) statements, such as this one:

if (a==1) function_a();
else
if (a==2) function_b();
else
if (a==3) function_c();
else
if (a==4) function_d();
else
if (a==5) function_e();

.. just by using the variable as a key (index) into a hash, so that you can directly call the corresponding function:

var mapping = {1:function_a, 2:function_b, 3:function_c, 4:function_d,5:function_e};
mapping[a] ();

This initializes first a hash map of values and maps a function "pointer" to each value, so that accessing mapping[x] will return the function pointer for the key "x".

Next, you can actually call the function by appending a list of function arguments (empty parentheses for no args) to the hash lookup.

Using this technique, you can reduce the complexity of huge conditional blocks. For example, consider this extract from weather_tile_management.nas:

if (code == "altocumulus_sky"){weather_tiles.set_altocumulus_tile();}
else if (code == "broken_layers") {weather_tiles.set_broken_layers_tile();}
else if (code == "stratus") {weather_tiles.set_overcast_stratus_tile();}
else if (code == "cumulus_sky") {weather_tiles.set_fair_weather_tile();}
else if (code == "gliders_sky") {weather_tiles.set_gliders_sky_tile();}
else if (code == "blue_thermals") {weather_tiles.set_blue_thermals_tile();}
else if (code == "summer_rain") {weather_tiles.set_summer_rain_tile();}
else if (code == "high_pressure_core") {weather_tiles.set_high_pressure_core_tile();}
else if (code == "high_pressure") {weather_tiles.set_high_pressure_tile();}
else if (code == "high_pressure_border") {weather_tiles.set_high_pressure_border_tile();}
else if (code == "low_pressure_border") {weather_tiles.set_low_pressure_border_tile();}
else if (code == "low_pressure") {weather_tiles.set_low_pressure_tile();}
else if (code == "low_pressure_core") {weather_tiles.set_low_pressure_core_tile();}
else if (code == "cold_sector") {weather_tiles.set_cold_sector_tile();}
else if (code == "warm_sector") {weather_tiles.set_warm_sector_tile();}
else if (code == "tropical_weather") {weather_tiles.set_tropical_weather_tile();}
else if (code == "test") {weather_tiles.set_4_8_stratus_tile();}
else ...

While this is not a very complex or huge block of code, it is an excellent example for very good naming conventions used already, because the consistency of naming variables and functions can pay off easily here, with just some very small changes, you can already reduce the whole thing to a hash lookup like this:

weather_tiles["set_"~code~"_tile"]();  # naming convention

This would dynamically concatenate a key consisting of "set_" + code + "_title" into the hash named weather_tiles, and then call the function that is returned from the hash lookup.

So for this to work you only need to enforce consistency when naming your functions (i.e. this would of course CURRENTLY fail when the variable code contains "test" because there is no such hash member (it's "4_8_stratus" instead).

The same applies to cumulus sky (fair weather), stratus/overcast stratus.

But these are very simple changes to do (just renaming these functions to match the existing conventions). When you do that, you can easily replace such huge IF statements and replace them with a single hash lookup and function call:

hash[key] (arguments...);

For example, consider:

var makeFuncString = func(c) return tolower("set_"~c~"_tile");
var isFunc = func(f) typeof(f)=='func';
var hasMethod = func(h,m) contains(h,m) and isFunc;
var callIfAvailable = func(hash, method, unavailable=func{} ) {
    var c=hasMethod(hash,makeFuncString(m) ) or unavailable();
    hash[makeFuncString(m)] ();
}
callIfAvailable( weather_tiles,code, func {die("key not found in hash or not a func");} );