Introduction
Ternary operator is a very useful syntactical notation
for condensing meaning. Basically it takes three
arguments, and syntactically speaking it's identifier is
split into two parts.
For example:
- Javascript conditionals
a ? b : c
- Math range check
a < b < c
- SQL joins
a JOIN b ON c
This is similar to why we
prefer infix binary operators (IBO), because they can be
chained together easily (or so called
associativity).
For example, most if not all of us will think that A is easier to read and understand than B:
A | B |
---|---|
a + b + c |
(+ (+ a b) c) |
Because IBO are so important,
most functional programming (FP) languages like Haskell
allow users to define custom IBO.
For example:
-- pipe forward operator
(|>) :: a -> (a -> b) -> b
a |> f = f a
-- example usage
main = [1, 2, 3]
|> map (+2)
|> filter (>3)
|> print
|> putStrLn -- [4, 5]
See more at Haskell Infix Operator.
Emulating with binary operator
However, when it comes to user-defined ternary operator,
none of the programming languages provide the mechanism.
Although ternary operator can be emulated using double
IBO, it is not without its own caveats.
Take this example from Haskell:
data Cond a = a :? a
infixl 0 ?
infixl 1 :?
(?) :: Bool -> Cond a -> a
True ? (x :? _) = x
False ? (_ :? y) = y
test = 1 < 2 ? "Yes" :? "No"
The caveat of such emulation is that partial usage is
not only syntactically valid, but also
semantically valid.
For example, we can omit that :?
part, and the compiler will not complain.
x = 1 < 2 ? "Yes"
From the user experience (UX) perspective, this is very bad,
because we intended users to always use ?
together with
?:
.
Emulating using mixfix operators
Other than using IBO, we can also emulate ternary operators using mixfix operators.
For example, in Agda:
-- Example function name _if_else_
-- (emulating Python's conditional operator)
_if_else_ : {A : Set} -> Bool -> A -> A -> A
x if true else y = x
x if false else y = y
The caveat of such approach is that users will be
required to lookup the syntax before they can even parse
a piece of code that is filled with mixfix operators.
Because, for example, the above _if_else_
operators can also be defined as:
if_then_else_ : {A : Set} -> Bool -> A -> A -> A
if x then true else y = x
if x then false else y = y
In this case, the user will not be able to parse the following code correctly without knowing the syntax of if
or/and else
:
x = a if b else c -- is this correct?
y = if a then b else c -- or this?
The Question
Due to the aforementioned caveats of emulating ternary
operators, I am on a quest for searching a mechanism to allow
user-definer ternary operators that is:
- Unambiguous (parsable by both machine and human)
- Universal (can be applied to all kinds of ternary operators)
Inspiration
I have been contemplating this question for a while, but
still there is no significant progress, until I read the
book The Relational Model of Database
Management
by the inventor of Relational Algebra, Edgar. F. Codd.
His notation for denoting theta-joins is ingenious.
For example,
SQL | Edgar's notation |
---|---|
X join Y on A join Z on B |
X [A] Y [B] Z |
The ingenious part about this is that he treats ternary
operator as decorated binary operators!
In the above example, the binary operator []
, is
decorated/tainted with A
, giving it a different
meaning.
This actually also relates to the mathematical notation of theta-join:
In this case, thetha symbol is decorating the join
symbol, turning it into a ternary operator that still
behave as a binary operator.
The advantage of this approach is that ternary
operators behave like binary operators, which means
that they can be chained together naturally.
The Potential Answer
By expanding on the idea where ternary operator are just
decorated/tainted binary operators, I first have this idea:
Ternary operators can be defined using the following syntax:
a X[ b ]Y c
Where a
, b
and c
are the arguments, and X[
and
]Y
are together the name of the ternary operators.
For example, let's define range-check:
-- Definition
a <[ b ]< c = a < b && b < c
-- Usage
print (1 <[2]< 3) -- true
print (1 <[3]< 2) -- false
Assuming [
]
is not used anywhere in the syntax of
the language, such ternary operators usage can be parsed
easily. Because whenever the user or machine sees [
or
]
, then they can anticipate a ternary operator.
However, the above syntax is obviously too noisy, so to
reduce the noise, we can swap the square brackets [
]
, with one of the most invisible ASCII operator, the backtick.
With this modification, the above range-check can be rewritten as follows:
-- Definition
a <` b `< c = a < b && b < c
-- Usage
print (1 <` 2 `< 3) -- true
print (1 <` 3 `< 2) -- true
Thus, the best mechanism that I can think of so far to define ternary operator is as follows:
a X` b `Y c
-- Where a, b and c are the arguments
-- while X` and `Y together is the name of the ternary operator
Also regarding precedence, ternary operators should have
lower precedence than binary operators, for example:
(a <` b + c `< d) MEANS (a <` (b + c) `< d)
Regarding, associativity, I will prefer
right-associativity over left-associativity, since it is
more common in most cases.
Therefore:
a X` b `Y c X` d `Y e = a X` b `Y (c X` d `Y e)
For example, the conditional ternary operator:
true then` b `else c = b
false then` b `else c = c
Where:
a then` b `else c then` d `else e
Should conventionally mean (right-associative):
a then` b `else (c then` d `else e)
Rather than (left-associative):
(a then` b `else c) then` d `else e
Conclusion
Hopefully this article has provided you some inspiration
on the mechanism of user-defined ternary operators.
Thanks for reading.
Top comments (0)