The Ternary Operator, Revisited

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:

  1. 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.

  2. 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.

The Treatment of Booleans in LSL

Booleans are people too! Well, George Boole was, anyway, and he is the one credited with and named for the mathematical operations performed on variables defined to carry one of only two states: true or false. Or on and off. Or zero and not-zero. Call those two states what you will. And always capitalize the adjective Boolean.

Some programming languages, such as Pascal, have a distinct Boolean type; there, it is defined as the enumeration (false, true), wherein, if you take the ordinal values of the enumerations, false maps to zero and true maps to 1.

LSL has no such Boolean type, as it follows the lead of the C-family languages in this regard. Here, a Boolean value is simply an interpretation of an expression, dependent on both its type and its value.

Let’s start with the seemingly obvious example, integers.

An integer value of zero can be interpreted to be false; to use current jargon, zero is a “falsy” value. Any non-zero integer can be interpreted to be true; -286, for instance, is a “truthy” value.

Try out this and all subsequent examples to confirm what you think should be the output:

[lsl]
default {
state_entry() {
if (0) llOwnerSay ("0 is truthy");
if (1) llOwnerSay ("1 is truthy");
if (286790) llOwnerSay ("286790 is truthy");
if (-1) llOwnerSay ("-1 is truthy");
}
}
[/lsl]

Complication #1: LSL defines two integer constants: FALSE and TRUE (letter-case is significant!). FALSE == 0, and TRUE == 1 (not any non-zero value!). This can lead to confusion in looking for values that are truthy:

[lsl]
default {
state_entry() {
if (0 == TRUE) llOwnerSay ("0 == TRUE is truthy");
if (1 == TRUE) llOwnerSay ("1 == TRUE is truthy");
if (286790 == TRUE) llOwnerSay ("286790 == TRUE is truthy");
if (-1 == TRUE) llOwnerSay ("-1 == TRUE is truthy");
}
}
[/lsl]

For example, -1 is indeed a value that can be interpreted to be true. But it’s not equal to the defined constant TRUE. -1 is a truthy value, but -1 != TRUE. On the other hand, 0 is a falsy value, and as well, 0 == FALSE. No, really.

Complication #2: comparison expressions using the so-called logical operators yield integer results. For integer operands, you’ll get 0 or 1, but don’t count on this with other types. For those, rely instead on getting zero or not-zero:

[lsl]
default {
state_entry() {
integer is_2_equal_to_77 = (2 == 77) ;
llOwnerSay ((string) is_2_equal_to_77) ;

integer is_list_nonEmpty = (["a", 42, <1,1,1>] != []) ;
llOwnerSay ((string) is_list_nonEmpty) ;
}
}
[/lsl]

Complication #3: types other than integers can have values that are truthy or falsy. The empty string (“”)—that is, a string with a character length of zero—is falsy; all other strings are truthy. the string “0” is truthy in LSL, though not necessarily so in other languages.

Keys are strings with a specified syntax and character length. Any string can be cast to a key type, but how it is interpreted is another matter. The NULL_KEY is supposed to be falsy and all other keys truthy. But watch out: keys do not yield integer values in conditional expressions, so you cannot use logical operators outside of == or !=. That means the operators || and && and ! cannot be used directly with keys.

[lsl]
// The NULL_KEY is "00000000-0000-0000-0000-000000000000" and is a string unless cast appropriately.

key keyCheck = NULL_KEY;

default {
state_entry() {
if (NULL_KEY) llOwnerSay ("This says a bare NULL_KEY is truthy.") ;
if ((key) NULL_KEY) llOwnerSay ("This says a NULL_KEY, cast as a key, is truthy.") ;
if (keyCheck) llOwnerSay ("This says NULL_KEY in a key variable is truthy.") ;
if ((string) NULL_KEY) llOwnerSay ("This says NULL_KEY, as a string, is truthy.") ;
}
}
[/lsl]

The ZERO_VECTOR (<0,0,0>) is falsy, all others truthy.

The ZERO_ROTATION (<0,0,0,1>) is falsy, all others truthy.

The empty list ([]) is falsy, all others truthy.

The float 0.0 is falsy, but I exhort you never to use floating-point numbers as Boolean values due to round-off errors. For this reason, in particular, never test a float for strict equality; always consider a tolerance range and test for inequalities.

// =============================================================

Now, I went through all that so I could talk about this.

A fundamental application of Boolean values is the on/off switch, which includes the mechanism by which on becomes off and vice-versa. If we use the defined constants, our code looks much like this:

[lsl]
integer g_switch = FALSE;

default {
touch_start (integer numDetected) {
if (g_switch) {
g_switch = FALSE;
// Turn something off here.
}
else {
g_switch = TRUE;
// Turn something on here.
}
}
}
[/lsl]

Since logical operations upon integer values yield TRUE or FALSE, we can take a small shortcut and flick the switch in one step:

[lsl]
integer g_switch = FALSE;

default {
touch_start (integer numDetected) {

// On becomes off and the other way around.
g_switch = ! g_switch;

if (g_switch) {
// Turn something on here.
}
else {
// Turn something off here.
}
}
}
[/lsl]

If, however, we elect to broaden our interpretation of true vs false by using truthy vs falsy values, then we can use arithmetic and/or bitwise operations:

[lsl]
integer g_switch = 0;

default {
touch_start (integer numDetected) {

// Granted this is overkill, but it is illustrative.
g_switch = (g_switch + 1) & 1 ;

if (g_switch) {
// Turn something on here.
}
else {
// Turn something off here.
}
}
}
[/lsl]

And if you’re building a three-way switch, then you cannot effectively use the two-way values TRUE and FALSE, since arithmetic is not defined, at least not philosophically, for Booleans:

[lsl]
integer g_switch = 0;

default {
touch_start (integer numDetected) {

g_switch = (g_switch + 1) % 3 ;

if (g_switch == 0) {
// Turn something off here.
}
else if (g_switch == 1) {
// Turn something on at the lower level.
}
else { // g_switch must == 2
// Turn something on at the higher level.
}
}
}
[/lsl]

My advice is never to mix these two treatments. If you begin using the constants TRUE and FALSE, then continue to assign these values to your Boolean variable as needed, or use the logical operators with integer operands to set the switch.

But if you use arithmetic or bitwise expressions to set the switch, then test for zero or other non-zero values explicitly, for you risk that the switch may not equal TRUE, even though it may be truthy.

Update

Oy. Never say never. I found an elegant exception to my advice above, which fills an expressive need, yet relies upon mapping the result of a logical expression to an integer value. This mapping in turn relies upon definitions which cannot change upon pain of backwards-incompatibility, so I think I’m safe.

Touch Events Across Obscuring Prims

Thanks to Simon Kline’s One-Prim HUD class, a basic but worthwhile introduction to the llDetectedTouchUV() function, I have spent the last couple days experimenting with tracking my position as I touch and drag across the surface of a cube. I employed the heretofore-seldom-used-by-me touch() event. No, not touch_start(). Touch(). This event fires continuously as you hold down the mouse button. Touch_start() fires once upon mouse-down, touch() fires continuously on mouse-hold, and touch_end() fires once upon mouse-up. The sequence does make a certain amount of sense.

During my experiments, I came across a situation where I was holding down the mouse button on an object while another object was moved to interpose itself between my camera view and the object I was supposedly touching.

What happens to the touch events in this situation? Since I am now holding my mouse down over another object, do the events cease? Do new events register on the obscuring object? I decided to find out, and the results surprised me.

Try it for yourself. Rez two cubes. Change their names: I called mine Root and Child. Then place the following script in each:

[lsl]
default {

touch_start (integer numDetected) {
llSay (0, "I have started a touch.");
}

// ——————————————————-

touch (integer numDetected) {
llSay (0, "I am continuing a touch.");
}

// ——————————————————-

touch_end (integer numDetected) {
llSay (0, "I have ended a touch.");
}
}
[/lsl]

Test these by touching and dragging across a surface of each prim. You’ll see one announcement for touch_start(), several announcements for touch(), and one last announcement for touch_end().

Now, move one prim in front of the other, partially obscuring it; I performed this by altering my camera position instead of moving the objects themselves. Mousedown on one prim, drag across to the other, and release your mouse over the other prim.

Notice the announcements in chat. No matter if you move to an obscuring prim, no matter if you move across prims that aren’t even touching, no matter if you release your mouse over land, all touch events (touch_start(), touch(), and touch_end()) occur in the proper sequence and are registered only with the prim where you began the sequence. By calling llDetectedTouchUV(), you can track your mouse’s coordinates on the prim when you can’t even see the object.

I get the same results whether the objects are linked or not, whether I compiled the scripts with the old LSL engine or Mono, or whether I’m using an official SL viewer or the Emerald viewer. Though these results surprised me, at least they’re consistent and repeatable, which I find very consoling.

The next question is, of what value is a touch_end() occurring off the prim? Well, that’s when you might call llDetectedTouchUV(), which will return valid coordinates on the prim, but component values of -1 off of it—again, even when the prim is obscured from view. You can see these coordinates by substituting the following event routine in your touch script:

[lsl]
touch (integer numDetected) {
vector point = llDetectedTouchUV (0);
llSay (0, "I am continuing a touch at U = " +
(string) point.x + ", V = " + (string) point.y);
}
[/lsl]

At least you can now tell when your mouse is off the prim, whether you can see it or not.

I have decided that this phenomenon, though initially unexpected by me, is highly desirable. In this manner I can direct actions to occur depending on my touch position upon the prim, and as long as I don’t let go, I can even obscure my view with other objects.

Concerning List Traversal With Node Deletion

Lists are funny things in LSL; their basic functions are not implemented as efficiently as they should have been. Furthermore, the very nature of call-by-value requires that lists be copied in their entirety as function parameters. Under the old interpreter, this often resulted in stack-heap errors for long lists.

Luckily things are a bit better under Mono. I have yet to receive a stack-heap error under Mono, but admittedly I may be in the habit of coding defensively now. And its implementation of lists now seems reasonable enough to forgo the obscure hacks developed even for simple list operations, such as the addition of an element.

Lists nevertheless are not arrays per se; any operation to construct, examine, and traverse lists will take a few more machine cycles than anyone has a desire to expect. Much of this comes from the need to call system functions explicitly to obtain any information about a list. So coders continue to look for ways to speed up the process.

I’ll set up a very basic loop which examines each node/entry:

[lsl]
Traverse_List (list myList) {
integer position;

for (position = 0; position < llGetListLength (myList); ++position) {
integer entry = llList2Integer (myList, position);
// Do something with the entry value here.
}
}

default {
state_entry() {
Traverse_List ([4, -87, -23, -145, 1, -812, -37, -790, 9, -71, 6]);
}
}
[/lsl]

Notice I have constructed a list of numbers in which each entry greater than one decimal digit in length is negative. This just permits a test condition which allows me to confirm the results of my code at a glance.

If I were simply to examine the list without modifying it, then I would isolate the loop invariant, the list length. Without researching the benchmarks, I’ll accept the hypothesis that it is far more efficient to cache an unchanging value in a local variable than to call a system function that returns the same value in every iteration. I’ll leave it to others to prove it.

While I’m at it, I’ll shave off a few cycles by initializing the loop variable at the time of declaration. I consider this simply a personal stylistic choice, acceptable because it does not violate the integrity of the control structure.

With these two changes in place, the script now looks like this:

[lsl]
Traverse_List (list myList) {
integer position = 0;
integer limit = llGetListLength (myList);

for (; position < limit; ++position) {
integer entry = llList2Integer (myList, position);
// Do something with the entry value here.
}
}

default {
state_entry() {
Traverse_List ([4, -87, -23, -145, 1, -812, -37, -790, 9, -71, 6]);
}
}
[/lsl]

Let’s filter this list to remove negative-number entries. My first reaction luckily coincides with good practice, that being to copy the nonnegative numbers into a new list, rather than deleting the negative numbers from my input:

[lsl]
list Ignore_Negatives (list myList) {
integer position = 0;
integer limit = llGetListLength (myList);
list newlist = [];

for (; position < limit; ++position) {
integer entry = llList2Integer (myList, position);
if (entry >= 0)
newlist += [entry];
}
return newlist;
}

default {
state_entry() {
list numbers = [4, -87, -23, -145, 1, -812, -37, -790, 9, -71, 6] ;
llOwnerSay ("Original list: " + llDumpList2String (numbers, ", "));

list no_negatives = Ignore_Negatives (numbers);
llOwnerSay ("Filtered list: " + llDumpList2String (no_negatives, ", "));
}
}
[/lsl]

By this time, I’m sure some of you out there are politely encouraging me to abandon the stereotypically good practice of encapsulating subroutine data into local variables, which requires the communication of global values through function parameters. That would certainly be da rigor in most languages.

LSL, of course, isn’t quite like most languages; it just looks like them. If LSL had implemented call-by-reference for its formal function parameters, then I would adopt this syntax for production code in a heartbeat; but it doesn’t, so normally I won’t. Though scalar variables such as integers benefit from encapsulation in LSL, for efficient memory management it is far more practical though far more dangerous to access a global list variable in a subroutine than it is to copy a potentially long list onto the limited stack as call-by-value requires. Sometimes it sucks to be us.

I will now stipulate that we don’t need the original list once it has been modified, so let’s try to work our magic upon the only copy of the input list. To simulate a more real virtual-world example, I will trigger the modification upon touch and move the subroutine code directly into the event. To ensure that this modification occurs only once, I will change state. And to access the same variable in different events, I will transfer the input list into the global scope:

[lsl]
list g_numbers = [4, -87, -23, -145, 1, -812, -37, -790, 9, -71, 6];

default {
state_entry() {
llOwnerSay ("Original list: " + llDumpList2String (g_numbers, ", "));
}

touch_start (integer numDetected) {
integer position = 0;
integer limit = llGetListLength (g_numbers);

// Will this delete negative numbers from the given input list?
for (; position < limit; ++position) {
if (llList2Integer (g_numbers, position) < 0)
g_numbers = llDeleteSubList (g_numbers, position, position);
}
state NEXT_STEP;
}

state_exit() {
llOwnerSay ("Filtered list: " + llDumpList2String (g_numbers, ", "));
}
}

state NEXT_STEP {
state_entry() {
// Perform the next step in the process.
}
}
[/lsl]

Pretty cool, huh? There’s only one problem: it doesn’t work. Yet. Before proceeding, see if you can tell where the problem lies.

(Feel free to insert your favorite quiz-show Muzak here.)

The problem is that I am modifying the list I am traversing. Once an entry is deleted, the list length is no longer a loop invariant, and the loop counter after its increment no longer points to the entry I expect within the list.

To fix this, the retrieval of the list length goes back into the loop header, and the loop counter is incremented only if the entry examined in that iteration is not deleted. The for header is then better written as a while:

[lsl]
list g_numbers = [4, -87, -23, -145, 1, -812, -37, -790, 9, -71, 6];

default {
state_entry() {
llOwnerSay ("Original list: " + llDumpList2String (g_numbers, ", "));
}

touch_start (integer numDetected) {
integer position = 0;

// This will delete negative numbers from the given input list.
while (position < llGetListLength (g_numbers)) {
if (llList2Integer (g_numbers, position) < 0)
g_numbers = llDeleteSubList (g_numbers, position, position);
else
++position;
}
state NEXT_STEP;
}

state_exit() {
llOwnerSay ("Filtered list: " + llDumpList2String (g_numbers, ", "));
}
}

state NEXT_STEP {
state_entry() {
// Perform the next step in the process.
}
}
[/lsl]

I have used the above model often, which produces the expected and desired effect. We can, however, avoid a conditional adjustment by stepping down the list instead of up. When an entry is deleted, we just decrement to the next lower entry unconditionally. Let’s focus on the loop, which now no longer needs to recompute the list length with every iteration:

[lsl]
integer position = llGetListLength (g_numbers);

while (–position >= 0) {
if (llList2Integer (g_numbers, position) < 0)
g_numbers = llDeleteSubList (g_numbers, position, position);
}
[/lsl]

Lately I came across a delightful, alternate coding for this same function, which is not so much a better method as it is an intriguingly different way of looking at the problem. Allow me to present this piece of resistance, which I place in the above framework. Here first is a conservative rendition, again focusing on the loop:

[lsl]
// Use the negative (two’s complement) of the list length.
integer position = – llGetListLength (g_numbers);

for (; position < 0; ++position) {
if (llList2Integer (g_numbers, position) < 0)
g_numbers = llDeleteSubList (g_numbers, position, position);
}
[/lsl]

The loop counter here, instead of a positive offset from the start of the list, ascends as a negative index from the end. The limit is zero, since the last entry in the list can be specified by an index of -1. When an entry is deleted, the negative index can be incremented without conditional adjustment.

Now, if you really want to get fancy, you can take advantage of the one’s complement of the length—that is, a reversal of all its bits—instead of using the negative, which is the two’s complement. This allows you to simplify the loop header even further:

[lsl]
// Use the one’s complement of the list length.
integer position = ~ llGetListLength (g_numbers);

while (++position) {
if (llList2Integer (g_numbers, position) < 0)
g_numbers = llDeleteSubList (g_numbers, position, position);
}
[/lsl]

Granted, this “backwards” way of looking at loop traversal requires a bit (as it were) of knowledge about the binary system and its manipulations, but the code is sound, well structured, and I think a tad more elegant than my version.

I think I’ll start using this implementation, just for fun.

A Vote For the Ternary Operator

On occasion I plan to discuss aspects of LSL language design, even though the practice may recognizably be futile. Creating a better language has already been demonstrated not to be on Linden Lab’s priority list. Yes, occasionally they’ll add a new function or two or three, and they’ll deprecate or remove outdated functions, but the syntax and semantics of the language are another matter entirely.

Take a look in the Jira, and you will see scads of complaints concerning perceived malfunctions of functionality. True, some of them are genuinely troublesome, as others are minor and esoteric.

I would hope nevertheless that the world would not stop trying to improve itself just because LL hasn’t diverted 100 percent of its attention to someone’s pet perceived bug.

Here’s one of my pet minor annoyances, which, like the tolerance of a sore tooth, only becomes more exasperating with time. Let’s consider the following example for a conditional assignment in LSL:
[lsl]
integer X;
if (condition)
X = 11;
else
X = 3;
[/lsl]
…and that doesn’t even include room for the optional braces.

To assign one value using a conditional statement, we’re actually writing X in three statements and five lines of code for what should be one trivial, unified, nearly atomic operation. In many languages, such as standard Pascal, this syntax is unavoidable.

Even when it is necessary, I have always found this pattern verbose and redundant. Instructors, however, will teach that this is the only true and proper way of writing this logic, as it is supposedly the most readable and most maintainable form. I believe this is not necessarily the case, and that this is just one reason why the Pascal language never really caught on.

(Yes, Pascal was never meant to be more than a teaching language. But then, neither was the original form of BASIC, and we all know what happened to that.)

I prefer other forms that attempt to balance readability with writability. One way to be a bit more concise in LSL is to declare and initialize within the same statement, afterwards performing a reassignment for the exception:
[lsl]
integer X = 11;
if (!condition) X = 3;
[/lsl]
Or, of course, depending on the expected distribution of the results of the condition:
[lsl]
integer X = 3;
if (condition) X = 11;
[/lsl]
So instead of using X in three statements, we’re down to two. Having come from an assembly-language background, I find this to be a reasonable method of handling a conditional expression, given the allowable syntax of variable declaration and assignment in LSL. I have read arguments saying that reassignment is inefficient and should be avoided at most, if not all, costs. But such a decision really depends on the context.

BTW, as a matter of style, you’ll notice I have lined up the left-hand side (LHS) of the equation whenever it makes the code prettier, and have written the IF statement on one line. I also reject the current fad of insisting that all IF/ELSE statements require braces for single statements after the condition. I will, however, agree that I may have developed a slightly more distinctive style than those advised by people who think they’re the only ones who are right.

Still, a conditional assignment ought to be more integrated yet less verbose. It would really be great if the LHS could be referenced only once, thereby emphasizing the unified nature of the operation. Also, I’d only have one typo to contend with, not possibly three.

The Ternary Solution

The C language was known from its original publication to be more writable–i.e, concise enough for its concepts–than most of its contemporaries, with the certain exception of APL, but let’s not go there. Its convenient syntax for auto-increment and side-effect assignment, for instance–both of which can be found in LSL–took some getting used to. Well, I got used to autoincrements rather quickly. Today, these innovations are expected in the syntax of C-family languages, though their adoption in production code even today is controversial: see Douglas Crockford’s arguments against using these two very techniques in JavaScript.

A welcome syntax in C was designed expressly for conditional assignment. It is a ternary operator (?:), effectively a concise if-then-else operation that returns a result, and is best used for clearly understandable, one-line expressions. Compare this pseudocode:
[lsl]
IF condition THEN return true_expression ELSE return false_expression
[/lsl]
with the syntax of the ternary operation:
[lsl]
condition ? true_expression : false_expression
[/lsl]
Outside of the initializtion of the condition, our example could be written, and really ought to be allowed to be written, in LSL as:
[lsl]
integer X = condition ? 11 : 3;
[/lsl]
The trivial operation that took five lines in the original example now takes only one. Now I can sleep at night. Except that I can’t, because in LSL I cannot express such a simple concept in this simple manner.

This operator is found at least in C/C++, Java, PHP, and JavaScript, and, from what I read, has recently been added using different syntax to Python and Visual Basic. Let me say this again: a conditional expression syntax now exists in Visual Basic, but not in LSL.

The addition of the ternary operator to the LSL syntax would be backwards-compatible. No new language is created except for philosophy majors. Existing programs would not break. It is an elegant, concise syntax that is also very readable.

A Jira ticket on this has already been submitted as MISC-271. Unfortunately, only six people have voted for it since its creation in June 2007. Hay, feel free to add your vote!

Update

I have encountered a syntax which, I believe, supplies a mechanism in keeping with the spirit of conditional expressions. It is slightly less readable than the classic ternary operator, but it requires no modification to LSL. That being the case: though I will keep my vote for this issue in place, I will no longer recommend the ticket to others.