Earlier I vented a certain frustration with the usual verbose syntax for a simple conditional assignment in LSL. Let me review the options from that post using a more general pseudo-code, yet still syntactically–though not semantically–correct in LSL. For the type name, I will use the word “whatever”:
[lsl] whatever result; if (some_logical_condition_is_TRUE) result = value_when_true; else result = value_when_false; [/lsl]
My argument was (and still is) that this is far too verbose for a such a simple action. Surely there must be a method a bit more concise but just as readable. Opinions on what is readable will vary, of course, but let’s see what the options are.
I first used a model from assembly-language programming: first assign to a default, then reassign based on the condition. This yields the following pattern:
[lsl] whatever result = value_when_false; if (some_logical_condition_is_TRUE) result = value_when_true; [/lsl]
I use this model quite a bit, yet I have heard objections that one shouldn’t allow reassignments as they correct a previously and unconditionally completed yet already erroneous action. I can understand this objection on philosophical grounds but not on practical grounds.
At this point I proposed, however futilely, the addition of an operator for conditional assignment to the syntax of LSL. Since this syntax resembles that of the C-family languages, it would make sense to adopt the latter’s ternary operator tokens and syntax. The above conditional assignment would then look like this:
[lsl] whatever result = some_logical_condition_is_TRUE ? value_when_true : value_when_false; [/lsl]
I honestly do not understand people’s objection to this syntax based on readability. I prefer it due to its elegant balance of readability and writability. It’s another tool in the C language, and there is nothing inherently wrong with it.
But like any tool, you need to know how to use it properly. There are caveats for this syntax with which I will agree wholeheartedly. For instance, it is really meant for simple expressions. If you can reasonably fit this expression on one line, great. If the expression must flow over one line, then be careful how you format the lines for readability. But beware of nesting ternary expressions. That would produce statements like this:
[lsl] x = a == b ? (c > d ? e : f) : (g <= h ? i : j); [/lsl]
You can imagine how this would look with longer variable names and/or greater levels of nesting. It is generally accepted that one shouldn’t nest ternary expressions. It has become a veritable taboo, as unto blinking upon web pages.
===================
My entire argument in favor of adoption was based on the assumption that there wasn’t a better, or at least equivalent, option already in LSL. It turns out my cursory conclusion may not have been oriented entirely in a rotation approaching reasonable veracity.
The solution I encountered depends, however, on one’s understanding of the relationship between the evaluation of logical expressions and the definitions and data types of the constants TRUE and FALSE. My recent exploration of Boolean expressions clarified these aspects for me.
In review: conditional expressions that explicitly employ logical operators result in either the integer value 1 (one, for true) or the integer value 0 (zero, for false), and, with one glaring exception (list inequality), no other value. In what I will accept at least to be a happy coincidence in LSL, the constant TRUE is defined to be the integer 1 (one) and FALSE to be 0 (zero).
Therefore, in this restricted circumstance, there is a map between the adjacent integers zero and one and what is true or false. That means we can use these two integers both as integer values and as Boolean values.
I normally don’t like doing that in LSL. It can get messy very quickly. I even stated firm advice in a previous post “never to mix these two treatments.” But now I believe I may have found a reasonable, philosophical exception to this rule.
The convenient trick here—and make no mistake: it is a trick—is to map the integer values of TRUE and FALSE to offsets in an LSL list.
As long as we can guarantee that the result of the conditional expression is either zero or one and nothing else, we can then use the evaluated expression as the integer offset to retrieve the appropriate value in a list.
Using our test syntax for this post, the conditional assignment looks like this:
[lsl] whatever result = llList2Whatever ([value_when_false, value_when_true], some_logical_condition) ; [/lsl]
If the logical condition is FALSE, then it equals zero, and value_when_false is retrieved. If the logical condition is TRUE, then it equals one, and value_when_true is retrieved. The retrieved value is then assigned to the variable result.
This only works when the conditional expression yields only TRUE or FALSE, that being 1 or 0. How can you guarantee that a conditional expression will yield only zero or one? Oh, we have a couple ways:
- Ensure the result will be the value of a logical operation by employing a logical operator explicitly: (x != 0), not just x; or (high >= low), not (high – low).
Again, beware of list inequality: (myList != []) yields the length of myList, not TRUE or FALSE. You might want to go with (llGetListLength (myList) != 0) here, or even ((myList != []) != 0), if you want to show off.
- Perform explicit bounds-checking, and either modify or reject values outside those bounds. To determine odd vs. even, for instance, use modulo 2 (llAbs (x % 2)) or, better, bitwise-and with one (x & 1). If I’m coding a switch where I want to use integer values instead of logical operators, I will write something like
switch = (switch + 1) & 1;.
As one further example, here are the before-and-after snippets for the same action. Before:
[lsl] vector sun = llGetSunDirection(); string light_label; if (sun.z >= 0.0) light_label = "day"; else light_label = "night"; [/lsl]
…and after:
[lsl] vector sun = llGetSunDirection() ; string light_label = llList2String (["night", "day"], sun.z >= 0.0) ; [/lsl]
Outside of the common statement to retrieve the sun’s direction vector, the five lines that comprise the conditional assignment in the Before version become one statement in the After version.
After careful yet speedy consideration, I can now state there is no need to seek enhancement of the LSL language to include the C-style ternary operator on the grounds that an equivalent method already exists in the language.
My arguments in favor of this syntax include at least this: that it encompasses a unit operation in a simple expression. And in this case, where the initial use is an assignment, at least I would have only one typo to correct, not hunt down three of the same.
Of course, detractors will be quick to point out the need for a system function call, which might be considered inefficient in a time-critical loop. But if your needs are so minutely time-critical, you might consider performing some benchmarks. I, for one, am not so concerned with efficiency to the point of becoming a mere cycle-shaver for its own sake.
Here’s the question that is the final test: is this syntax at least equally readable, if not more so, than the original multi-statement code fragment that began this post? Since I know of no truly objective test which will answer this question, I will simply state that, for myself, this solution is both readable and elegant. I will first consider this syntax wherever simple conditional assignments are called for.
Well, that’s the plan, anyway.
Further update, 11 July 2021: Whoa, it’s been a while. There appears to be one barrier to my use of the list syntax as a wholesale replacement or alternative to the ternary operator: in the former, both true and false values in the list are evaluated, whereas in a true ternary statement, either the true-clause or the false-clause is evaluated, but not both! This makes the list syntax appropriate only for simple expressions without side-effects, such as scalar values.
Nope, it looks like I must take up again the lost cause to advocate for a true ternary operator. Hey, I can dream.