+ 2 3
Is a perfectly valid binary operator.
A binary operator is simply one that takes two operands, their relative to positions do not matter one bit
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.
`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)
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.
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.? 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.
x ?? y == x // when x is not null
null ?? y == y
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.
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.
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.
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.
This is exactly how `1 to 10 by 2` works in scala.
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...
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.
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³".
I'd rather say that a binary operator _has_ an arity of 2 or talk about the arity _of_ an operator.
how does it hinge tho?
(x f1 y) f2 ((x f3 y) f4 (x f5 y))
FMA(x,y,z) ~ x += y*z
You see it all over the place in numerical computing and deep learning.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.
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.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.
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.
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.
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.
> 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-rangeFrom 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.
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.
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.array[addr +: W]
or
array[addr -: W]
The former chooses W elements beginning at addr, the latter chooses W elements ending at addr.
WITH a AS b c
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.
(cond) ifTrue: [then-block] ifFalse: [else-block]
For some reason the way Swift handles syntax I find less memorable or composable. >>> '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'
I guess = is taken by another symbol, but that does assignment all on its own.
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.) 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)
say my $x=42; #42
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.
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 SELECT count() FROM hits
WHERE EventDate BETWEEN '2024-01-01' AND '2024-11-11'
https://clickhouse.com/docs/en/sql-reference/operators