Ternary Operators
65 points by azhenley 2 days ago | 68 comments
  • xn 2 days ago |
    What about subject–predicate–object triples?
  • readthenotes1 2 days ago |
    Binary operators don't "go between" 2 operands, they just consume two operands.

    + 2 3

    Is a perfectly valid binary operator.

    • BerislavLopac 2 days ago |
      This falls into the third category, "Functions/methods that prefix any number of arguments". That the binary operators "go between" the operands is literally the definition used in the article.
      • readthenotes1 a day ago |
        That's my point. That is not a good definition of a binary operator.

        A binary operator is simply one that takes two operands, their relative to positions do not matter one bit

        • BerislavLopac a day ago |
          Well, I suppose that's one of the definitions; Hillel opted for the other one. :shrug:

          I think that he wanted to emphasize the distinction between operators and functions; in your definition, there is no difference between "binary operator" and "function with two arguments". Of course, some languages limit what characters can be used in function names, but that is certainly not universally true.

  • mjw1007 2 days ago |
    JavaScript method call notation is arguably a ternary operator, or a candidate for the "Near Misses" list (because « foo.bar(x) » isn't the same as « var meth = foo.bar; meth(x) »).
    • marcosdumay 2 days ago |
      Javascript's `this` is the gift that keeps giving...
    • sltkr 2 days ago |
      Not technically ternary because the number of arguments is variable, but definitely an interesting case.

      `a.b(c)` is actually a bit of a degenerate case because `b` must be an identifier, it cannot be an arbitrary expression, which seems to disqualify it as a true operator. But actually `a.b` is just syntactic sugar for `a["b"]` and you can view []() as an operator, so you can write nonsense like:

          [function(){return this[1]}, 42][0]()
          
          {foo(a){return this.x*a; }, bar(a){return this.y*a;}, x: 1, y:2} [ Math.random() < 0.5 ? 'foo' : 'bar' ] (100 + 20 + 3)
  • Cieric 2 days ago |
    I'm not sure I'd say that it can't be done. In some languages it could be implemented if the language itself supports optionals. So "bool ? x" could result in an Optional<type(x)>, while "Optional<type(x)> : y" could be the typical implementation of value_or that is found on optionals.

    Whether it's a good idea to implement it that way is something I haven't come to an opinion on yet. My only argument is that "can't be decomposed" feels like it's a little to far reaching.

    Edit: to add on to this, while it's not really a ternary operator in the sense he seems to be looking for. It it common for assembly to have fma instructions. But this feels like a really weird edge case since x * y + z technically can be decomposed into the 2 separate operators, but the fma instruction itself can't be decomposed without losing precision.

    • Someone 2 days ago |
      > I'm not sure I'd say that it can't be done. In some languages it could be implemented if the language itself supports optionals. So "bool ? x" could result in an Optional<type(x)>, while "Optional<type(x)> : y" could be the typical implementation of value_or that is found on optionals.

      It is tricky because, in

        flag ? x : y;
      
      exactly one of x and y gets evaluated.

      So, flag ? x would have to evaluate flag first, and only evaluate x if that has a truthy value.

      That can be tricky in many languages, for example if the programmer writes

         (b ≠ 0) ? a ÷ b : 0;
      
      You’d need lazy evaluation of arguments or something similar.
      • Izkata 2 days ago |
        You and GP are basically describing "bool && x" and "bool || y" (for the languages that return the value instead of strict booleans when evaluating && and ||)
  • pistoleer 2 days ago |
    After some thinking, the ternary conditional operator can be decomposed into 2 composing binary operators like such:

    ? takes a bool, a T, and returns option<T>

    true?b == Result b

    false?b == None

    : takes an Option<T> and a T and returns T

    Result x : y == x

    None : y == y

    However, in most languages (looking at you php) the ?: act as a type of parenthesis: in a?b:c, any expression goes into b, no matter it's precedence.

    • masfuerte 2 days ago |
      Nice. Your : operator is roughly the C# null-coalescing operator ??.

          x ?? y == x    // when x is not null
      
          null ?? y == y
    • cwp 2 days ago |
      Nice. Another aspect of the ternary operator is conditional evaluation. Beyond parenthesis, in a?b:c, only one of b and c get evaluated.
  • taeric 2 days ago |
    Agreed with a sibling that this is somewhat abusing the terminology. In/pre/postfix of the operators is a different thing from how many terms an operator acts on. Indeed, in RPN, it was not uncommon to try and execute an operator but not have enough terms on the stack.

    Seems that we typically call the ?: combination a ternary operator largely stems from the distinction between operators, functions, and keywords. One that we largely don't keep in a consistent fashion. Add in functions versus methods, and things get really fun.

    Programming also has the odd case where functions have an easy syntax to say that there are multiple inputs, but we don't have a common syntax to say there are multiple outputs. Which is something that is quite common in every domain.

    • reichstein 2 days ago |
      I'm personally convinced that the reason the conditional operator of called "the ternary operator" it's that the ANSI C Programming Language book contains the phrase "the ternary operator, ?:", and a lot of readers didn't know what "ternary" meant and thought it was a name.
      • taeric 2 days ago |
        This sounds reasonable to me. I used to think you could guess what someone's first programming language was based on if they wrote methods or procedures. :D
  • zarzavat 2 days ago |
    clamp and mix are good candidates. Ternary operator syntaxes are rarer than ternary operations, because there's not much that can justify being so syntactically indulgent.

    Conditionals are common enough that they can justify the indulgence, but even then I prefer an if-else expression syntax over ?:, it generalises better to other constructs such as pattern matching.

    • jcparkyn 2 days ago |
      > Conditionals are common enough that they can justify the indulgence

      I think there's another much more important factor that distinguishes conditionals from most other ternary operations (clamp, mix, FMA, etc): The "conditional evaluation" part, which is what makes it hard to replicate with a regular function call.

  • kragen 2 days ago |
    Maybe it was Dave Long that pointed out that lerp is a generalization of the conditional ternary operator. When the condition is 0, it returns the from-point, and when it's 1, it returns the to-point, but if you generalize it to the whole real number line, you lose nothing semantically except for short-circuiting. Lerp is a pretty common function in graphics—the simplest way to understand Bézier splines is as recursive lerping—but for whatever reason it generally isn't written as an infix operator, which seems to be what Hillel is looking for here.

    I think that Hillel's criterion that the ternary operator "can't be decomposed into two sequential binary operators" is too demanding. He says, "bool ? x makes no sense on its own, nor does x : y". Well, that's just because C's grammar is written such that they won't parse. If you extend the grammar to permit them, it's not very hard to extend the semantics as well. For example, you could permit every expression to either succeed, yielding a value, or fail, yielding no value; languages that work in something like this way include regular expressions, META-II, Icon, Unicon, and Prolog, though each of them deviates from it in detail. Then, to get the C/JS semantics, bool ? x succeeds with the value of x if bool is true, or fails if bool is false, and x : y succeeds with the value of x if x succeeds, but if x fails, does y.

    You could model the exception mechanism in languages with exceptions as failing to return a value, in which case x : y ends up being a general-purpose exception-handling mechanism, so you can say something like total[k] = (total[k] : 0) + 1 to increment a possibly-nonexistent hash key. Mark Lentczner's language Wheat has an operator like this, spelled !!, which I stole for my own language Bicicleta.

    (That's assuming you parse bool ? x : y as (bool ? x) : y. You can also invent semantics that give you the right behavior with the parsing bool ? (x : y) but the ones that occur to me seem less appealing.)

    Similarly for Elixir's stepped range 1..10//2. Hillel says, "This isn't decomposable into two binary ops because you can't assign the range to a value and then step the value later." Well, maybe not (I don't know Elixir), but it seems clear enough what r//2 should do if r is the name of a range value: it should produce a new range stepping either by 2 or by twice the previous step.

    C++ does something like this for Hillel's (or munificent's) example of a[b] = c: the expression a[b] evaluates to a reference, which in rvalue context decays to an ordinary value, but which can indeed be assigned to a variable. Unfortunately, C++'s reference semantics are somewhat weak, with the consequence that actually overriding operator[] in C++ to return a reference is pretty bug-prone, because you can autovivify things you didn't want to autovivify.

    By Hillel's apparent syntactic and decomposability criteria, I think every single two-argument method in Smalltalk is a "ternary operator". 1 to: anInt do: [ :i | r := ( r * self ) ], for example, or arr at: i put: v. As he sort of points out, Objective-C inherited this from its Smalltalk roots, but he sort of throws up his hands at it.

    • n_plus_1_acc 2 days ago |
      > Hillel says, "This isn't decomposable into two binary ops because you can't assign the range to a value and then step the value later." Well, maybe not (I don't know Elixir), but it seems clear enough what r//2 should do if r is the name of a range value: it should produce a new range stepping either by 2 or by twice the previous step.

      This is exactly how `1 to 10 by 2` works in scala.

    • abecedarius a day ago |
      > Maybe it was Dave Long that pointed out that lerp is a generalization

      iirc it was email among the three of us where I brought that up and pointed out that identities about Bézier splines looked pretty with infix lerp notation. (Blurry memory as usual, though.)

      > to get the C/JS semantics, bool ? x succeeds with the value of x if bool is true, or fails if bool is false, and x : y succeeds with the value of x if x succeeds, but if x fails, does y.

      There's a potential snag in that if x in (bool ? x) can fail then (true ? x : y) could run y. Of course this can be addressed...

      • kragen a day ago |
        Apologies! I didn't mean to steal your valor! My memory is pretty bad these days.

        My intent with the split between the true/false world of values and the success/failure world was to avoid the situation where a false x would run y, but you're right that if bool ? x can fail (for example if x fails), we haven't really solved anything. And if a failing x doesn't cause bool ? x to fail, we have some other knotty problems: does it return a value, and if so, what? Doesn't that eliminate the possibility of using the failure construct as a model of exceptions? (Is that bad?)

        It's fun to think about what semantics you'd need to decompose lerp into two such operations. Maybe (a/b) : (c/d) evaluates to (a+c)/(b+d), for example.

  • Etheryte 2 days ago |
    The whole article hinges on the fact that the author seems to be unfamiliar with the term arity (?), which feels weird given they're clearly familiar with a number of programming languages etc. The definitions for unary, binary, etc are rather arbitrary and not how they're usually used.
    • hwayne 2 days ago |
      I am amazed that I somehow didn't use the word arity once in the email.

      That said, I don't believe I'm using the terms unary and binary wrong. "Unary" historically meant "a math operation that takes one parameter" and "binary" meant "operation that takes two". An algebraic group, for example, is defined as a set and a binary operation that follows certain properties.

      arity is a way of generalizing unary/binary to arbitrary parameters. It is equally correct to say that `+` is a binary operation and to say that it is a 2-arity operator. It's like how "nth power" generalizes "square" and "cube": "9 cubed" is the same as "9³".

      • reichstein 2 days ago |
        Agree, other than that I wouldn't use "2-arity". Maybe "2-ary", but that's just "binary" written confusingly. It works for "n-ary".

        I'd rather say that a binary operator _has_ an arity of 2 or talk about the arity _of_ an operator.

    • yen223 2 days ago |
      "Unary" and "binary" were used exactly how they are used when talking about programming languages.
    • NooneAtAll3 2 days ago |
      > The whole article hinges on the fact that the author seems to be unfamiliar with the term arity (?)

      how does it hinge tho?

  • lilyball 2 days ago |
    When talking about J forks, the author says they don't know how to parse `x (f1 f2 f3 f4 f5) y`. After reading the relevant documentation, I believe this parses as

      (x f1 y) f2 ((x f3 y) f4 (x f5 y))
    • abrudz 2 days ago |
      Correct. Now try parsing `x (f1 f2 f3 f4 f5 f6) y`!
  • zusammen 2 days ago |
    I’m surprised not to see fused multiply-add here:

        FMA(x,y,z) ~ x += y*z
    
    You see it all over the place in numerical computing and deep learning.
    • achierius 2 days ago |
      It'd be syntactic sugar for two binary operations: `tmp = y*z; x += tmp`
      • atq2119 2 days ago |
        It actually isn't, that's what makes it fused.

        If you separate the operations like that, the intermediate result is rounded. The final result is then often different from the result of the FMA, which doesn't do the intermediate rounding.

      • NooneAtAll3 2 days ago |
        in hardware it isn't sugar - there's (pipeline) gains to be gained from literally "fusing" the two into a single circuit
  • bediger4000 2 days ago |
    In Tarski's axiomatization of plane geometry, there's a betweenness triadic relationship, Bxyz, point y is between x and z.
  • zokier 2 days ago |
    (postgre)sql has lots of quirky stuff that kinda qualifies. For example

         substring ( string text [ FROM start integer ] [ FOR count integer ] ) → text
    
    at a glance you might think that as just a normal function call, but the arguments do not follow the regular function call argument syntax, instead it has this special sauce ternary syntax.
  • seanhunter 2 days ago |
    What about the derivative operator in maths? When you write say d^nf/dx^n in Leibniz notation (or the equivalent in Lagrange notation f^(n)(x)), you have 3 arguments. The "f" is the function you are taking the derivative of, the n is what derivative to take and then the (x) is what to name the argument in the resulting function.

    Obviously in the common case you're just writing something like f'(x) or f''(x) where the ' stands for ^(1) but it's still a ternary operator.

    Another common example in maths is the "limit" operator where you have lim_x->a <someexpr> and the arguments are x, a and someexpr.

    • taeric 2 days ago |
      The article touches on sigma notation and such. So, I would expect that is largely how they would treat it.

      The pitfall you will run into quickly, is that we have very loose definitions between operations and functions. Is akin to wanting a sharp distinction between expressions and terms or statements. There are widely accepted distinctions that typically work, but this is largely convention based with no real intrinsic quality that defines them.

      • seanhunter a day ago |
        Sigma, capital pi and the definite integral are interesting.

        What counts for arity of an operator? For sigma you have 4 things I would argue.

        - the low and high limits of the index

        - the name of the index variable in the expression you're summing

        - the expression itself

        The name of the index variable doesn't seem to me to be of the same kind as the others in that it's sort of a dummy argument but it's generally needed to make the syntax work[1].

        [1] yeah I know people sometimes just write the limits above and below and rely on convention for people to know the name is n or i in the same way they do for the square bracket or big pipe notation (I don't know what that's called) you learn when you learn to take definite integrals.

        • taeric a day ago |
          varargs are a thing for a reason. Same with keyword arguments.

          The elephant in this discussion remains the idea that operations are somehow different from functions. This is akin to asking what makes a datatype a primitive. There are some that are typically included in that list, but there is no real intrinsic quality of any data that makes it more so.

          • seanhunter a day ago |
            Well yes completely agree. In TFA the author makes some distinctions which seem to me to be more or less arbitrary along these lines. eg Python's "range(min, max,step)" is a ternary operator as far as the author is concerned but but regex substitution "s/pat/subst/flags" is a "datatype constructor" rather than an operator. Not sure why a constructor isn't an operator if range is an operator, especially as the python documentation describes range() as creating an immutable sequence type.

               >  class range(start, stop, step=1)
               >
               >    Rather than being a function, range is actually an immutable sequence type, as documented in Ranges and Sequence Types — list, tuple, range.
            
            https://docs.python.org/3/library/functions.html#func-range

            From a maths perspective as far as I can see for most "normal maths" an operator is just something which does a mapping between the input domain(s) to the output domain(s) ie it's almost exactly a function. I think when you get to functional analysis there's a more concrete definition in terms of operator algebras but I haven't got there yet myself.

    • crdrost 2 days ago |
      On the one hand the n'th derivative operator is clearly a binary operator that you are composing with a function application operator.

      On the other hand, OP seems to have not noticed that the ternary conditional also has this structure. That is, `f : x` could be syntactic sugar for `f(() => x)` [or perhaps `f(p => p, () => x)` if you like Church encodings] and then there are a few options for meanings of `x ? y` that have `(x ? y) : z` as a conditional.

      • seanhunter a day ago |
        You need to be able to specify how many derivatives you're taking and what variable(s) you're taking the derivative with respect to. That's two arguments you've partially applied if you have something you can just apply to a function application operator.
        • crdrost a day ago |
          So I want to clarify that I don't think you're wrong, I just think that that's like saying + is a ternary operator because of course x +^{n} y, which is x plus y n times, and therefore equal to x + (n × y), has three symbols that are related by the terms.

          When I called this out you started talking now about something a bit different, partial derivatives, which accept two integer indices and not just one. But behold, the symmetrical version of +^n is +^{m, n}, such that

              x +^{m,n} y = m x + n y.
          
          It’s not that any of this is wrong, ya dig? Just that we're counting to three to feel satisfied when there isn't much satisfaction to be gained.
          • seanhunter a day ago |
            Got it. Yes, agree.
  • not2b 2 days ago |
    Verilog has a sliding-window part select, which produces a slice that has a fixed width. It's an lvalue expression (it can be assigned to). It's ternary.

    array[addr +: W]

    or

    array[addr -: W]

    The former chooses W elements beginning at addr, the latter chooses W elements ending at addr.

    • o11c 2 days ago |
      GDB has `arraylike[start]@length`. But `arraylike[start:end]`, where `end=start+length` is more common across languages.
  • ks2048 2 days ago |
    Like others here, I find the way this article discusses things a bit odd. I think a more interesting question is to emphasize SYNTAX (the word doesn’t seem to appear in the article). Which languages have special syntax that maps to an AST node as OP(a,b,c)? Focus on syntax and things like range(start,end,step) dont fall into this category.
    • plaidphantom 2 days ago |
      I don't think they're saying that such a range function is itself a ternary, but using that as a lead-in to discussing the Frink ("1 to 100 step 15") and Elixir ("1..10//2") syntaxes.
  • plaidphantom 2 days ago |
    How about SQL joins? "foo JOIN bar ON foo.id = bar.fooid"
    • recursive 2 days ago |
      If that counts, how about common table alias declarations?

      WITH a AS b c

  • o11c 2 days ago |
    It mentions ranges, but not slices?

    Note that the "conditional/selection operator" is normally thought of as a logical operator, but variants exist that operate bitwise `r = (if_false & ~condmask) | (if_true & condmask)` or on a vector. Additionally, there maybe a special versions that operate at compile-time instead of runtime.

    Modular exponentiation is commonly implemented as ternary for major performance improvements, even though technically it can be done as composition of two binaries.

    Modular congruence `3 === 1 (mod 2)` is common in math.

    Many arithmetic and bitwise operations take a third argument for the carry bit. Rotates are notable for actually performing a different operation in that case.

    There are various SIMD operations.

    There's also SSA.

  • karmakaze 2 days ago |
    I rather like the Smalltalk message selector structure Boolean>>ifTrue:ifFalse:

      (cond) ifTrue: [then-block] ifFalse: [else-block]
    
    For some reason the way Swift handles syntax I find less memorable or composable.
  • Izkata 2 days ago |
    Missed python's slicing:

      >>> 'abcdefghijklmnopqrstuvwxyz'[1:20:4]
      'bfjnr'
    
    It's start/end/skip, with default values of 0, length, 1; each can be omitted so it's rare to see the third one unless someone is showing off how to reverse a string:

      >>> 'abcdefghijklmnopqrstuvwxyz'[::-1]
      'zyxwvutsrqponmlkjihgfedcba'
    
    Though really these are slice objects, it's just the syntax only works inside array access:

      >>> slice
      <class 'slice'>
      >>> z = slice(1, 20, 4)
      >>> 'abcdefghijklmnopqrstuvwxyz'[z]
      'bfjnr'
    • JadeNB 2 days ago |
      I think this may fall (conceptually) under the range operators.
  • JadeNB 2 days ago |
    I don't understand why `a += b` is called binary (I agree!) but `a f= b` would be called ternary. I guess it's thinking of `f` as one of the arguments?
    • Izkata 2 days ago |
      The way they described it, "f" can be almost any function, so it's the third argument to "=", while "+=" in most languages you can't replace "+" with anything - those are a specific collection of two-character operators.
      • extraduder_ire a day ago |
        What binary infix operator can't the + in += be replaced by? I can't think of any. /,*,-,%,^,||,&&,|,& all work.

        I guess = is taken by another symbol, but that does assignment all on its own.

        • Izkata a day ago |
          They're describing "+=" as a single operator, not a combination. "-=" is a different single operator, as is "/=" and and all the others. The first character and the "=" are inseparable, they just have all the combinations defined as independent operators. They're comparing it to a language where the first part (which can be multiple characters!) is actually separate from "=".
  • abrudz 2 days ago |
    APL brings another couple of ternaries (besides for those mentioned with regards to J):

      x f[k] y
    
    This one is universal among traditional APLs (though some modern APLs, including J, remove it). However, it is traditionally only used for a concept of application along an axis of APL's multidimentional arrays, and only for a strictly limited set of built-in "operators". That said, GNU APL and NARS2000 both allow it on user-defined functions (which have infix syntax, just like "operators").

      x n.f y
    
    This is infix application of the function f from the namespace n, so n must have namespace value, but it is still a proper argument because n isn't just an identifier, but can also be an expression:

      x (expression).f y
    
    In fact, the expression can be an entire (multidimentional!) array of namespaces:

      x (namespace1 namespace2).f y
    
    is equivalent to

      (x[1] namespace1.f y[1])(x[2] namespace2.f y[2])
    
    Furthermore, f can be a built-in "operator" which appears to exist in every namespace, but which also takes the current namespace into consideration. For example, x⍳y is the position of y in x and if io0 is a namespace wherein indexing is 0-based and io1 is a namespace wherein indexing is 1-based, we can write:

      'AX' 'BX' (ns0 ns1).⍳ 'X'
    
    and get the result 1 2 because X is at position 1 in AX with 0-based indexing but at position 2 of BX with 1-based indexing. (I'm not saying you should write code like this, but you could.)
  • adonovan 2 days ago |
    Why doesn’t the article mention the most obvious example of all, s[i:j]?
  • dan-robertson 2 days ago |
    First example that came to mind for me is in SQL:

      x IS BETWEEN y AND z
    
    A not-quite-binary example would be languages where comparisons chain:

      x < y <= z
        Equivalent to:
      (x < y) && (y <= z)
        (Possibly modulo short-circuiting)
    
    In maths, you would sometimes see clever constructions with reversed relation symbols to succinctly describe many things at once; when written down I would sometimes see relations going out in other directions after left and right were exhausted.

    The ‘ternary’ assignment is customisable in OCaml: x.(y) <- z is (maybe was?) exactly equivalent Array.set x y z, including in the parse tree, and so could be can be customised by shadowing the Array module; and custom variants may be defined too, e.g.

      type 'a t = {mutable a : 'a array; mutable p : int }
      
      let push t x =
        ensure_capacity t 1;
        t.a.(t.p) <- x;
        t.p <- t.p + 1
    
      let (.:()<-) t j x =
        if j >= t.p then raise Out_of_bounds;
        t.a.(j) <- x
    
      (* one can now use t.:(i) <- x *)
    
    Another weirder mixfix example would be binding operators, but then you accept lots of other syntax too like if then else. Binding operators let you define weird functions with names like (let) or (and) and then:

      let* x = y in z
      (* equivalent to: *)
      (let*) y (fun x -> z)
    
      let* x1 = y1 and x2 = y2 in z
      (* equivalent to: *)
      (let*) ((and*) y1 y2) (fun (x1, x2) -> z)
    • librasteve a day ago |
      yes, in raku that’s spelled

        say my $x=42;  #42
  • jcarrano 2 days ago |
    They are all just functions. There is really no distinction between an operator and a function in programming languages. And by using currying you could even do with just unary operators.
    • EE84M3i 2 days ago |
      I thought that was only true in languages with lazy evaluation? Otherwise how do you implement short-circuiting operators?
  • rurban a day ago |
    Beware the number of evaluation dragons!

    1 < x < 5 is not just syntactic sugar for 1 < x and x < 5.

    Every lisp macro programmer knows that x must be evaluated only once, so it needs to be gensym'ed. Only if x is a constant or evaluates to itself it can be optimized without a gensym.

  • librasteve a day ago |
    interesting collection… I would propose the raku sequence op `…` as a ternary, for example:

      1,2,3 … Inf;   #(1,2,3,4,5,6…) arithmetic sequence
      1,2,4 … Inf;   #(1,2,4,8,16,32…) geometric sequence 
    
    so looks at all three of the operands to work out the result
  • zX41ZdbW a day ago |
    SQL has a BETWEEN AND operator:

        SELECT count() FROM hits
        WHERE EventDate BETWEEN '2024-01-01' AND '2024-11-11'
    
    https://clickhouse.com/docs/en/sql-reference/operators