14
$\begingroup$

In many programming languages, there is a ternary operator such as X ? Y : Z or Y if X else Z. This can be used as an if expression.

However, I was wondering what other ternary operators are possible and useful. I was looking for operations that come up frequently enough to warrant their own syntax or keywords that take in three operands.

$\endgroup$
1
  • $\begingroup$ I guess this assumes that your language makes a syntactic distinction between "operators" and other arbitrary functions. $\endgroup$ Oct 3, 2023 at 3:24

7 Answers 7

13
$\begingroup$

Wikipedia lists the following ones, which I let here operate on arguments a, b and c:

  • The Multiply–accumulate operation, which is commonly used in digital signal processing. In pseudo-code: a += b * c
  • The 'between' operator, as used e.g. in SQL: b <= a <= c
  • A range iterator with step size, for (int i = a; i < b; i += c)
$\endgroup$
5
  • 5
    $\begingroup$ I guess in python the full form of the slice operator a[b:c:d] is basically one quadranary operator $\endgroup$
    – mousetail
    May 18, 2023 at 10:52
  • $\begingroup$ @mousetail No, I wouldn't say so. b:c:d is a separate syntax which creates a slice object, and that object gets passed to a's __getitem__ method. (In earlier versions, b, c, d could become separate arguments for a method.) So the [] is a separate operator that's part of a compound expression. This is also evident from the actual parse grammar. Similarly, the only reason for treating multiply-accumulate as a single operation is because there is hardware support for doing so. $\endgroup$ Jul 1, 2023 at 14:51
  • 1
    $\begingroup$ @KarlKnechtel You can't create a slice using this syntax in any other situation though $\endgroup$
    – mousetail
    Jul 1, 2023 at 14:59
  • $\begingroup$ Hmm, well, there is that. :) $\endgroup$ Jul 1, 2023 at 15:07
  • $\begingroup$ It's hard to imagine a more compact syntax for the multiply-accumulate operation. $\endgroup$
    – Barmar
    Oct 3, 2023 at 20:34
9
$\begingroup$

Any function of three arguments could, in principle, be made into an operator; but usually they are not operators, because few functions are so important that they need a special syntax. In specialised languages some operations might be important enough, e.g. perhaps "lerp" or "clamp" for graphics applications. Otherwise these operations can be provided as named functions in the standard library, and then not only do they not take up syntax space, but the compiler doesn't need to know how to implement them.

The conditional expression is a bit of a special case, because it does branching, so it can't be equivalently written as a function whose arguments are evaluated eagerly. Other operations which branch include the short-circuiting logical operators and/&& and or/||, but these are binary rather than ternary.

But perhaps there could be other branching ternary operators; a good place to look would be common standard library functions which accept a callback in order to avoid eager evaluation. Some suggestions:

  • "Get or default" from a dictionary. If a normal indexed access gives a null value when the key is missing, then this can be written like dictionary[key] ?? default_value where ?? is a null-coalescing binary operator, but if a missing key raises an error instead then this wouldn't be an option, so a ternary operator could be useful.
  • "Compute if not present" in a dictionary, which likewise could be written like dictionary[key] ??= default_value with a null-coalescing assignment operator, but only if a missing key results in a null value rather than an error.

Another one which occurs to me is a logical operator which checks whether at least two of three operands are true, sometimes called the majority function. As a function, maj(a, b, c) would evaluate c unnecessarily when both a and b are already true (or both already false), but there is no convenient way to write something equivalent using just the short-circuiting binary logical operators, so perhaps some languages would benefit from this being a ternary operator.

Finally, slicing a sequence like a[b:c] could be implemented as a ternary operator; some languages (e.g. Python) decompose this into an indexed access where the index is a "slice" object representing the range b:c, but if you don't want to create "slices" as first-class objects in your language then this could be a ternary operator instead.

$\endgroup$
3
  • 2
    $\begingroup$ Something which isn't obvious, but becomes clear when you think about it, is that assignment is actually a ternary operation, with the three operands being the scope, the cell, and the value. Ioke and Seph are two languages that execute assignment a = b as a message send to the current implicit receiver with two arguments: =(a, b). So, it is written as a binary operator but executed as a ternary. (Arguments are passed un-evaluated and the method implementation can choose whether to evaluate them or inspect them raw, hence a does not raise an error.) $\endgroup$ Jul 1, 2023 at 12:19
  • $\begingroup$ Another useful operator, especially for graphics but also in some control algorithms, is (((x ^ z) & y) ^ z (but with z only evaluated once). This is also very useful to have as an atomic pseudo-primitive (which would typically be built from Compare-and-swap, CompareExchange, or other lower-level primitives.) $\endgroup$
    – supercat
    Oct 2, 2023 at 21:43
  • 1
    $\begingroup$ @supercat Sounds like that could be an answer. $\endgroup$
    – kaya3
    Oct 2, 2023 at 21:46
5
$\begingroup$

Ioke and Seph have something called trinary operators.

Trinary operators in Ioke and Seph are operators that are written as binary operators but executed as ternary operators. In particular, the default behavior of the trinary operators (like all operators in Ioke and Seph, they can be overridden) is assignment.

One of the fundamental goals of Ioke and Seph is to have a small and simple kernel and implement as much as possible through message sending. Even assignment is implemented as a message send.

When you write

a = b

I.e. the binary infix assignment operator, that is actually syntactic sugar for

=(a, b)

I.e. sending the message = with the un-evaluated argument a (denoting the name of the cell you want to assign to) and the evaluated argument b (the value you want to assign) to the current ground (which is somewhat of a hybrid of a Smalltalk-style self or this and a Scheme-style lexical closure context). So, when you write the binary assignment operator with the variable on the left-hand side and the value on the right-hand side, you are actually executing a ternary operator with the first operand being the current context, the second argument being the name of the variable, and the third being the value.

$\endgroup$
1
  • 1
    $\begingroup$ That's... pretty wild, although vaguely reminiscent of some ideas I've had myself. $\endgroup$ Jul 1, 2023 at 14:55
5
$\begingroup$

While I'm unaware of any languages that actually work this way, it could conceivably be useful to treat repeated applications of a binary operator as if they were a ternary (really, n-ary) operator.

For example, suppose the grammar recognizes "term" mathematical expressions formed by + or -; it could be useful to transform e.g. a + b - c into a.sum_with(b, -c) at compile time1. This way, for example, container types could offer an optimized implementation for concatenation, without needing to hard-code that into the runtime.

1 Here, the method name sum_with is hard-coded as being specially for implementing this arithmetic; maybe it's spelled sum, or __sum__, or operator+, etc. Similarly, maybe the arguments would be passed in a new container object, rather than variadically. None of these implementation details matter to the underlying idea.

$\endgroup$
2
  • $\begingroup$ Defining the semantics of unary negation for containers is left as an exercise, of course. ;) $\endgroup$ Jul 1, 2023 at 15:04
  • 2
    $\begingroup$ Mathematica works like this. a+b+c is transformed into Plus[a,b,c]. $\endgroup$
    – alephalpha
    Jul 3, 2023 at 2:46
2
$\begingroup$

Many graphics and control applications could benefit from a bitwise variation of the ? : operator, and a compound-assignment variation, though the most natural syntax for assignment operator would put the operands in a different order from ? : (the non-assignment form could be patterned after ? : as shown below, or after the assignment form).

unsigned cookieCutter(unsigned mask, unsigned op0, unsigned op1)
{
  return (((op0 ^ op1) & mask) ^ op1);
}

// For non-atomic destination
unsigned cookieCutterAssign(unsigned *dest, unsigned mask, unsigned newBits)
{
  *dest = (((newBits ^ *dest) & mask) ^ *dest);
}

// For atomic destination
unsigned cookieCutterAssign(unsigned *dest, unsigned mask, unsigned newBits)
{
  unsigned temp;
  do
  {
    temp = *dest;
  } while(!__compareAndSwap(dest, ((newBits ^ temp) & mask) ^ temp, temp);
}

The need to use one of the values twice in the computation makes such constructs awkward to write using binary bitwise operators.

$\endgroup$
1
$\begingroup$

In lower-level languages, the basic arithmetic and logical operators are often chosen to mirror the operations available as hardware CPU instructions. This makes it particularly easy for a naive compiler to recognize when the corresponding instructions can be used. For instance, on a machine with a read-modify-write add instruction, C's += operator is valuable, because even a non-optimizing compiler can see that it should emit the RMW version. Abstractly, += is unnecessary because x += y is equivalent to x = x + y, but a non-optimizing compiler would likely emit three instructions (load, add, store) for the latter.

In that spirit, I thought I'd look through the ARM64 base instruction set for instructions with three or more input operands, as potential candidates for ternary operators in a language. Here's a summary, with C-like syntax to describe the operation. x,y,z,w denote register inputs (i.e. run-time values), a,b,c,d are immediates (compile-time constants).

  • Shifted arithmetic and bitwise operations. Most of the basic ALU instructions have an option to run one of the inputs through a barrel shifter first, so we have a single instruction that can compute x + (y << a). The << can be replaced with right-shift (logical or arithmetic) or with rotate. This option is available for the operations + - & | ^ as well as inverted bitwise operations &~ |~ ^~ (e.g. the bit-clear instruction BIC computes x & ~(y << a))

  • Bitfield move BFM. This is actually a quaternary operator, which replaces bits a through b of the value x with the low-order bits of y, or vice versa.

  • Conditional select CSEL. Here the flags register is an implicit input, so it's effectively the familiar conditional operator x ? y : z. One interesting wrinkle is that there are variants CSINC, CSNEG, CSINV which respectively do x ? y : z+1, x ? y : -z, and x ? y : ~z.

  • Conditional-compare CCMP. This is kind of hard to describe in C terms, but roughly does something like x ? (y < z) : a. The constant input a looks redundant in C, but the point is that a may set a different condition code than y < z would.

  • Add with carry ADC. If we again think of the carry flag as an implicit input, then this does x + y + z where z can only be 0 or 1. There is also SBC which does x - y - z.

  • Multiply-add MADD and multiply-subtract MSUB. These do x + (y * z) and x - (y * z). A fun fact is that ARM64 does not actually have a simple multiply instruction; the MUL mnemonic is an alias of MADD with x taken as the zero register xzr whose value is always zero.

  • Extract EXTR. This is basically right-shift, but with a double-wide 128-bit input that spans two 64-bit registers. So it doesn't really count for a higher-level language, which would just denote as x >> a where x was of a 128-bit type.

  • Store STR with base and index. Does x[y] = z. There is also store-pair which does x[a]=y, x[a+1]=z, but as with EXTR above, this is basically x[a]=y where y is 128 bits.

  • Test bit and conditional branch TBNZ/TBZ. They do if (x & (1 << a)) goto b; and if (!(x & (1 << a))) goto b; These wouldn't likely show up in a higher-level language, because the target of a branch is not usually an explicit expression; instead, it's determined by the block structure of the code.

I didn't go through the floating-point and SIMD instruction sets carefully, but a couple worth mentioning are:

  • floating point multiply-add FMADD/FMSUB

  • BSL which is supercat's "cookie cutter" operation, basically bitwise ?:.

  • A variety of fancy bitwise operations for accelerating CRC / SHA / AES.

$\endgroup$
1
$\begingroup$

In SQL, as well as the BETWEEN operator already mentioned, I tend to regard the joins themselves as ternary operators.

A LEFT JOIN ... ON takes two table operands, plus a control expression.

$\endgroup$

Not the answer you're looking for? Browse other questions tagged .