Ternary Operator Left Associativity

Ternary operator left associativity

In any sane language, the ternary operator is right-associative, such that you would expect your code to be interpreted like this:

$a = 2;
echo ($a == 1 ? 'one' :
($a == 2 ? 'two' :
($a == 3 ? 'three' :
($a == 4 ? 'four' : 'other')))); # prints 'two'

However, the PHP ternary operator is weirdly left-associative, such that your code is actually equivalent to this:

<?php
$a = 2;
echo (((($a == 1 ? 'one' :
$a == 2) ? 'two' :
$a == 3) ? 'three' :
$a == 4) ? 'four' : 'other'); # prints 'four'

In case it still isn't clear, the evaluation goes like this:

echo ((((FALSE    ? 'one' :
TRUE) ? 'two' :
$a == 3) ? 'three' :
$a == 4) ? 'four' : 'other');

echo ((( TRUE ? 'two' :
$a == 3) ? 'three' :
$a == 4) ? 'four' : 'other');

echo (( 'two' ? 'three' :
$a == 4) ? 'four' : 'other');

echo ( 'three' ? 'four' : 'other');

echo 'four';

Ternary Operator Associativity

The ternary operator is always right-associative

The ternary operator is right-associative, as we see in your first example (and as we see below, this is the only choice we have if we want to let the corresponding if-else blocks to contain anything else than expressions that evaluate to booleans). Note that your second example leaves no room associativity, so this does it does not show an example of any left-associativity of the ternary operator.

/* example 1 */
a ? b : c ? d : e
==> { right-ass. } => a ? b : (c ? d : e), OK
==> { left-ass. } => (a ? b : c) ? d : e
/* |___|
\ not OK: since then both of these must be
booleans: that is restriction we don't have */

/* example 2 */
a ? b ? c : d : e
==> { only-valid-splitup } => a ? (b ? c : d) : e

(a ? b ? c) : d : e
/* |___|
\ not valid, there's no ? ? ternary operator */

a ? b ? (c : d : e)
/* |___|
\ not valid, there's no : : ternary operator */

Hence, the ternary operator's associativity is well-defined even if you nest ternary operator expressions. Take care, however, that doing so tends decrease code readability, and it is even outright recommended against this in the Language Guide - Basic Operators

...

Use the ternary conditional operator with care, however. Its
conciseness can lead to hard-to-read code if overused. Avoid
combining multiple instances of the ternary conditional operator into
one compound statement
.

The ternary operator: not two unary operators, but a unique operator of its own

The ternary operator is a special operator not really directly related to any of the other operators in Swift; all other belong to the families of unary and binary operators.

  • Unary operators ...

  • Binary operators ...

  • Ternary operators operate on three targets. Like C, Swift has only one ternary operator, the ternary conditional operator (a ? b : c).

From the Language Guide - Basic Operators.

Since we're only allowed to customly define our own prefix (unary) and infix (binary) operators in Swift, I suspect you will have quite difficult time implementing your own true ternary operator, since you're limited to using it as two separate unary infix operators ? and :, which is naturally not the same as a single ternary operator. (You could always look at this somewhat old blog post by Nate Cook, explaining how to mimic a ternary operator by using two binary operators, however as currying is to be removed in Swift 3, I don't know if this will be possible for future Swift versions).

Ternary operator as both left operative and right operative.. or neither

Since the ternary conditional operator has its own place in the operator precedence table, (i.e. no other operator has exactly the same precedence as it), the associativity rule only applies when disambiguating a conditional operator from another.

Right to left associativity means that the implicit parentheses are around the rightmost ternary.

That is,

a ? b : c ? d : e

is equivalent to

a ? b : (c ? d : e).

https://en.wikipedia.org/wiki/Operator_associativity is a useful link.

Why is the conditional operator right associative?

If it evaluated from left to right, it'd look like this:

z = ((a == b ? a : b) ? c : d);

That is, it would use the result of the first conditional (a or b) as the boolean condition of the second conditional. That doesn't make much sense: that's like saying:

int z, tmp;
/* first conditional */
if(a == b) tmp = a;
else tmp = b;
/* second conditional */
if(tmp) z = c;
else z = d;

While perhaps one day you'll want to do exactly this, it's far more likely that each ?: that follows is meant to add more conditions, like if / else if / else if / else, which is what the right-associative binding yields:

int z;
/* first conditional */
if(a == b) z = a;
else /* second conditional */ if(b) z = c;
else z = d;

Nested Ternary-operator Associativity in php vs java

Left- vs right-associative is about precedence when stringing multiple operators together, e.g. A + B + C.

For the ternary operator A ? B : C, this only has meaning when the extra ternary operator replaces the C part (or A part):

A ? B : X ? Y : Z
(A ? B : X) ? Y : Z <-- left-associative (PHP)
A ? B : (X ? Y : Z) <-- right-associative (Java, C, C++, C#, Perl)

If the extra ternary operator is inserted in the middle (replacing B), it can only have one meaning:

A ? X ? Y : Z : C
A ? (X ? Y : Z) : C

That is why PHP and Java agree on the second one. There is no left-/right-associative rule applied.

Ternary operator associativity in C# - can I rely on it?

Yes, you can rely on this (not only in C# but in all (that I know) other languages (except PHP … go figure) with a conditional operator) and your use-case is actually a pretty common practice although some people abhor it.

The relevant section in ECMA-334 (the C# standard) is 14.13 §3:

The conditional operator is right-associative, meaning that operations are grouped from right to left.
[Example: An expression of the form a ? b : c ? d : e is evaluated as a ? b : (c ? d : e). end
example]

How does associativity of operator work during evaluation of expression?

Associativity tells you what happens if you have multiple instances of the same operator in a row. For example,

f() - g() - h()

parses as

(f() - g()) - h()

and not

f() - (g() - h())

because - is left associative, not right associative.

None of this has anything to do with evaluation order, which determines which function is called first.

As for ?: being right associative, it means

a ? b : c ? d : e

parses as

a ? b : (c ? d : e)

(This makes slightly more sense if you think of ?...: as a single operator.)

However, ?: guarantees left-to-right evaluation: The first operand is always evaluated first, then exactly one of the other operands (depending on the truth value of the first result).


In your example,

c=a>b? a=a*2: a=a+3

(please never put assignments inside ?: like that in real code) is parsed as

c = ((a>b) ? (a=a*2) : (a=a+3))

This is entirely due to precedence, not associativity (we don't have multiple identical operators next to each other here).

a>b is evaluated first (yielding false), which causes a=a+3 to be evaluated (yielding 4), which is then assigned to c.

C++, conditional operator associativity

If ?: was left associative, the statement

finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";

would be treated as

finalgrade = ((grade > 90) ? "high pass" : (grade < 60)) ? "fail" : "pass";

which (in this case) would not compile, since "high pass" and (grade < 60) have different types.

Since it is actually right associative, the statement is treated as

finalgrade = (grade > 90) ? "high pass" : ((grade < 60) ? "fail" : "pass");

Concatenated ternary operators with pyparsing

I think this operator is actually right-associative, not left. If I change your code to:

import pyparsing as pp

TERNARY_INFIX = pp.infixNotation(
pp.pyparsing_common.integer, [
(("?", ":"), 3, pp.opAssoc.RIGHT),
])

TERNARY_INFIX.runTests("""\
1?1:(0?1:0)
(1?1:0)?1:0
1?1:0?1:0
""", fullDump=False)

Then I get reasonable output, and no error for the input without parens:

1?1:(0?1:0)
[[1, '?', 1, ':', [0, '?', 1, ':', 0]]]

(1?1:0)?1:0
[[[1, '?', 1, ':', 0], '?', 1, ':', 0]]

1?1:0?1:0
[[1, '?', 1, ':', [0, '?', 1, ':', 0]]]

Here is a larger expression to evaluate the largest of 3 variables (from this C tutorial: http://cprogramming.language-tutorial.com/2012/01/biggest-of-3-numbers-using-ternary.html):

TERNARY = pp.infixNotation(
pp.Char("abc"), [
(pp.oneOf("> <"), 2, pp.opAssoc.LEFT),
(("?", ":"), 3, pp.opAssoc.RIGHT),
])
TERNARY.runTests("""\
(a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c)
a > b ? a > c ? a : c : b > c ? b : c
""", fullDump=False)

Gives:

(a > b) ? ((a > c) ? a : c) : ((b > c) ? b : c)
[[['a', '>', 'b'], '?', [['a', '>', 'c'], '?', 'a', ':', 'c'], ':', [['b', '>', 'c'], '?', 'b', ':', 'c']]]

a > b ? a > c ? a : c : b > c ? b : c
[[['a', '>', 'b'], '?', [['a', '>', 'c'], '?', 'a', ':', 'c'], ':', [['b', '>', 'c'], '?', 'b', ':', 'c']]]

EDIT: I see now that this a similar situation to repeated binary operators, like "1 + 2 + 3". Left-associative, pyparsing parses them not as [['1' '+' '2'] '+' '3'], but just ['1' '+' '2' '+' '3'], and it is up to the evaulator to do the repetitive left-to-right evaluation.

When I added the ternary operator, I did not envision a chained form such as the one you are parsing. A one-line change to infixNotation will parse your expression successfully with left-associativity, but like the chained binary operators gives an ungrouped result:

[1, '?', 1, ':', 0, '?', 1, ':', 0]

Like the repeated addition example, it is up to the evaluator to do the successive left-to-right evaluation, somcething like:

def eval_ternary(tokens):
operands = tokens[0]
ret = bool(operands[0])
i = 1
while i < len(operands):
ret = bool(operands[i+1]) if ret else bool(operands[i+3])
i += 4
return ret

If you want to hand-patch your pyparsing code, change:

       elif arity == 3:
matchExpr = _FB(
lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr
) + Group(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr)

to:

       elif arity == 3:
matchExpr = _FB(
lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr
) + Group(lastExpr + OneOrMore(opExpr1 + lastExpr + opExpr2 + lastExpr))
^^^^^^^^^^

Make this change in pyparsing.py, or copy the definition of infxNotation into your own code and change it there.

I'll make this change in the next release of pyparsing.

EDIT - Fixed in pyparsing 2.4.6, just released.



Related Topics



Leave a reply



Submit