I previously contemplated elimination of the ternary conditional operator, but now I have a solution that retains and improves it. My biggest concern here is having a single unifying conditional logic. I do not desire distinct syntax nor behaviour for expression evaluation and flow. I also want only syntax that feels right. The solution draws a few concepts together and breaks apart the the ternary into binary operations.
[sourcecode]
//traditional ternary conditional (with Leaf ?| syntax)
var a : integer = cond ? 1 | 2
// binary conditional (well defined)
var b : optional integer = cond ? 1
//binary or (well defined)
var c : integer = b | 2
[/sourcecode]
The conditional operator ?
Unifying expression logic with flow logic requires a way to express conditions without an else clause. Consider a typical C style ‘if’ statement.
[sourcecode]
if( cond ) {
//block
}
[/sourcecode]
Leaf has a ‘do’ statement which works with conditionals. Originally I just coded them as below.
[sourcecode]
do cond ? {
//block
}
[/sourcecode]
But it really bothered me that outside of ‘do’ the ‘cond ? {…}’ part didn’t have any meaning. As Leaf should be a high-level orthogonal language that is quite troublesome.
The solution is to give the ‘?’ operator a binary definition, which may be perplexing since the expresison must result in a value whether the condition is true or not. This can be done by using the ‘optional’ type. ‘cond ? a’ results in an optional set to ‘a’ or an unset optional.
[sourcecode]
var a : optional integer
//a true condition sets the optional to the provided value
a = true ? 5
assert( has(a) and a == 5 )
//a false value results in an unset option (has no value)
a = false ? 6
assert( not has(a) )
[/sourcecode]
I find this solution very attractive. It gives a clear binary meaning to the ‘?’ operator. It doesn’t introduce a new concept, instead building upon the current ‘optional’ types. It provides a quick syntax for producing those optional values. And it allows for a strong functional definition of what the ‘do’ statement actually does: when provided an optional code block it executes the code if set, otherwise it does nothing. A code block is mereley an anonymous closure, which also allows the following code.
[sourcecode]
//code is like a variable, so we can assign it
var actions = {
//block
}
//the execute it directly
do actions
//or conditionally
do cond ? actions
//for more fun we can create an optional code block now
var maybe_actions = cond ? actions
//do executes if has(maybe_actions) otherwise it does nothing
do maybe_actions
[/sourcecode]
This syntax is not yet supported in Leaf, but it definitely will be. I need to clarify how local closures (captures by reference) work first.
The optional or operator |
Having done a lot of JavaScript recently I’ve become accustomed to the ‘||’ operator, which takes a value if define, or an alternate if not. The trouble is that JavaScript doesn’t distinguish between undefined, false, or zero, so the use of ‘||’ is unfortunately a bit limited. Nonetheless I like the basic idea. To adopt to Leaf the operator needs to be strictly typed, yet still have the notion of being unset. Again the ‘optional’ type can be used.
When used on an optional value in Leaf the ‘|’ evaluates to the value of the optional if set, or an alternate value when unset.
[sourcecode]
//note by default an optional is unset
var a : integer optional
//b gets the value 5 since a is not set
var b = a | 5
//now b will get assigned 4 since a is set to this value
a = 4
b = a | 6
[/sourcecode]
Being a strictly typed language requires that, unlike JavaScript, both the set and default value must resolve to the same type. I don’t see this as a limitation since that is almost always how it is used. It should also be noted that “resolve to” allows for conversion sequences as well: the operator can be used so long as both operands can be unified to the same type. This has the interesting consequence of allowing one to chain several optional values together.
[sourcecode]
//a,b,c,d are all optional integers
//e gets assigned the first set value, or 5 if all unset
e = a | b | c | d | 5
[/sourcecode]
The choice of the symbol ‘|’ was made for a few reasons. It is short, and as conditionals and optionals are a cornerstone of the language this brevity will be appreciated. It has this ‘or’ meaning in other contexts: in C it meant bit-wise or; in JavaScript and others a double bar already has our desired meaning.
The ternary operator
These two operators can be combined together to get the conditional ternary operator.
[sourcecode]
//a,b,c are integers
a = cond ? b | c
[/sourcecode]
First the ‘cond ? b’ resolves to an optional integer: unset if ‘cond’ is false, set to ‘b’ if ‘cond’ is true. Next that optional is used with the ‘|’ operator. If the optional was set it takes the value, otherwise it takes ‘c’. All together this means if ‘cond’ is true, then ‘b’ is used, otherwise ‘c’ is used.
It also achieves the unification goal: both expression and block flow can use the same syntax. In the below code the same behaviour is followed, except the values are code-blocks rather than integers. The ‘do’ statment then just executes the result of the expression.
[sourcecode]
do cond ? {
//then-block
} | {
//else-block
}
[/sourcecode]
Lvalue problem
There is one complex issue that arises, which I’ll only discusss briefly here. The above all works fine for rvalues. I would however like to support the following syntax.
[sourcecode]
cond ? a | b = 12
[/sourcecode]
This requires the left-side expresison to resolve to an lvalue. Unfortunately, the ‘?’ creates an optional which drops the lvalue status of ‘a’. There are two ways to address this problem, both of which result in the same simple syntax (but more complex syntax could vary). First, we could allow an optional to retain the lvalue of what it is wrapping. This is quite problematic in the Leaf compiler right now. Perhaps once I’ve introduced formal reference types it could be made to work.
The second option is to make this a ternary operator, which is the current solution. When used on their own the ‘?’ and ‘|’ operators are binary. When used together they are a ternary operator. The ternary operator can avoid creating the temporary optional and evaluate directly to ‘a’ or ‘b’, thus easily preserving the lvalue status.
Select and chaining
The binary and ternary versions both allow chaining so you could produce long lists of comparisons. I don’t like the syntax once the expression gets very long. To alleviate this I’ve created an extra ‘select’ operator which is a clearer and less ambiguous representation.
[sourcecode]
q = select(
cond_a ? value_a,
cond_b ? value_b,
cond_c ? value_c,
value_d
)
[/sourcecode]
The behaviour of ‘select’ is easy to define: it takes the first set, or non-optional argument. Note the first three arguments resolve to optionals, and the last one does not. To retain lvalue status, we’ll allow the select to bypass the optional creation so long as the final item is not optional.
Again here the expression may resolve to a code-block. When combined with ‘do’, ‘select’ becomes a basic form of a traditional ‘switch’ statement.
[sourcecode]
do select(
cond_a ? {
//block-a
},
cond_b ? {
//block-b
},
{
//default block
}
)
[/sourcecode]
No default block is required, without one the ‘select’ may simply resolve to an unset optional, for which ‘do’ does nothing.
Conclusion
Overall I’m quite happy with this approach. It fits nicely with the optional types and unifies conditional expressions and flow. There’s a bit of an issue about lvalue’s, and while I will probably revisit it later, it definitely works for now.
Followup
It’s been pointed out to me that the JavaScript ‘&&’ operator behaves very similar to my ‘?’ operator. If the boolean conversion of the left operand is false, then it evaluatest to the left side, otherwise the right side. I’m not sure I’m a big fan of this though, since JavaScript then lacks a proper boolean-and operator: ‘&&’ is only guaranteed to return boolean if both the left and right side are actually booleans.
Lua appears to use the same behaviour as JavaScript, though with the operator names ‘and’ and ‘or’.