34

If I want to compare two numbers (or other well-ordered entities), I would do so with x < y. If I want to compare three of them, the high-school algebra student will suggest trying x < y < z. The programmer in me will then respond with "no, that's not valid, you have to do x < y && y < z".

Most languages I've come across don't seem to support this syntax, which is odd given how common it is in mathematics. Python is a notable exception. JavaScript looks like an exception, but it's really just an unfortunate by-product of operator precedence and implicit conversions; in node.js, 1 < 3 < 2 evaluates to true, because it's really (1 < 3) < 2 === true < 2 === 1 < 2.

So, my question is this: Why is x < y < z not commonly available in programming languages, with the expected semantics?

  • 1
    Here's the grammar file, which they handily stick right in the Python documentation - I don't think it's that difficult: docs.python.org/reference/grammar.html – Aaron Hall Apr 27 '16 at 22:07
  • I don't know other languages as well as I know Python, but I can speak to the simplicity of Python's interpretation of it. Perhaps I should answer. But I disagree with gnasher729's conclusion about it doing damage. – Aaron Hall Apr 27 '16 at 22:17
  • @ErikEidt - The demand is being able to write mathematical expressions the way we were taught in high school (or earlier). Everyone who is mathematically inclined knows what $a<b<c<d$ means. Just because a feature exists does not mean you have to use it. Those who don't like it can always make a personal or project rule banning its use. – David Hammen Apr 28 '16 at 1:07
  • 2
    I think what it comes down to is that it's better for the C# team (as an example) to explore LINQ and in the future maybe record types and pattern matching than it is to add some syntactic sugar that would save people 4 keystrokes and not really add any expressiveness (you can also write helpermethods like static bool IsInRange<T>(this T candidate, T lower, T upper) where T : IComparable<T> if it really bothers you to see &&s) – sara Apr 28 '16 at 14:54
  • 1
    SQL is quite "mainstream" and you can write "x between 1 and 10" – JoelFan May 3 '16 at 22:36
30

These are binary operators, which when chained, normally and naturally produce an abstract syntax tree like:

normal abstract syntax tree for binary operators

When evaluated (which you do from the leaves up), this produces a boolean result from x < y, then you get a type error trying to do boolean < z. In order for x < y < z to work as you discussed, you have to create a special case in the compiler to produce a syntax tree like:

special case syntax tree

Not that it isn't possible to do this. It obviously is, but it adds some complexity to the parser for a case that doesn't really come up that often. You're basically creating a symbol that sometimes acts like a binary operator and sometimes effectively acts like a ternary operator, with all the implications of error handling and such that entails. That adds a lot of space for things to go wrong that language designers would rather avoid if possible.

  • 1
    "then you get a type error trying to do boolean < z" - not if the compiler allows for chaining by evaluating y in-place for the z comparison. "That adds a lot of space for things to go wrong that language designers would rather avoid if possible." Actually, Python has no problem doing this, and the logic for parsing is confined to a single function: hg.python.org/cpython/file/tip/Python/ast.c#l1122 - not a lot of space for things to go wrong. "sometimes acts like a binary operator and sometimes effectively acts like a trinary operator," In Python, the whole comparison chain is ternary. – Aaron Hall Apr 28 '16 at 12:51
  • 2
    I never said it wasn't doable, just extra work with extra complexity. Other languages don't have to write any separate code just for handling their comparison operators. You get it for free with other binary operators. You just have to specify their precedence. – Karl Bielefeldt Apr 28 '16 at 14:18
  • Yes, but ... there is already a Ternary operator available in a lot of languages? – JensG Apr 28 '16 at 14:23
  • 1
    @JensG The denotation of ternary means that it takes 3 arguments. In your link's context, it's a ternary condition operator. Apparently "trinary" in a term coined for an operator that appears to take 2 but actually takes 3. My primary issue with this answer is it is mostly FUD. – Aaron Hall Apr 28 '16 at 14:27
  • 2
    I'm one of the downvoters on this accepted answer. (@JesseTG: Please unaccept this answer.) This question confuses what x<y<z means, or more importantly, x<y<=z. This answer interprets x<y<z as a trinary operator. That is exactly how this well-defined mathematical expression should not be interpreted. x<y<z is instead shorthand for (x<y)&&(y<z). The individual comparisons are still binary. – David Hammen Apr 28 '16 at 18:41
37

Why is x < y < z not commonly available in programming languages?

In this answer I conclude that

  • although this construct is trivial to implement in a language's grammar and creates value for language users,
  • the primary reasons that this does not exist in most languages is due to its importance relative to other features and the unwillingness of the languages' governing bodies to either
    • upset users with potentially breaking changes
    • to move to implement the feature (i.e.: laziness).

Introduction

I can speak from a Pythonist's perspective on this question. I am a user of a language with this feature and I like to study the implementation details of the language. Beyond this, I am somewhat familiar with the process of changing languages like C and C++ (the ISO standard is governed by committee and versioned by year.) and I have watched both Ruby and Python implement breaking changes.

Python's documentation and implementation

From the docs/grammar, we see that we can chain any number of expressions with comparison operators:

comparison    ::=  or_expr ( comp_operator or_expr )*
comp_operator ::=  "<" | ">" | "==" | ">=" | "<=" | "!="
                   | "is" ["not"] | ["not"] "in"

and the documentation further states:

Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to x < y and y <= z, except that y is evaluated only once (but in both cases z is not evaluated at all when x < y is found to be false).

Logical Equivalence

So

result = (x < y <= z)

is logically equivalent in terms of evaluation of x, y, and z, with the exception that y is evaluated twice:

x_lessthan_y = (x < y)
if x_lessthan_y:       # z is evaluated contingent on x < y being True
    y_lessthan_z = (y <= z)
    result = y_lessthan_z
else:
    result = x_lessthan_y

Again, the difference is that y is evaluated only one time with (x < y <= z).

(Note, the parentheses are completely unnecessary and redundant, but I used them for the benefit of those coming from other languages, and the above code is quite legal Python.)

Inspecting the parsed Abstract Syntax Tree

We can inspect how Python parses chained comparison operators:

>>> import ast
>>> node_obj = ast.parse('"foo" < "bar" <= "baz"')
>>> ast.dump(node_obj)
"Module(body=[Expr(value=Compare(left=Str(s='foo'), ops=[Lt(), LtE()],
 comparators=[Str(s='bar'), Str(s='baz')]))])"

So we can see that this really isn't difficult for Python or any other language to parse.

>>> ast.dump(node_obj, annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE()], [Str('bar'), Str('baz')]))])"
>>> ast.dump(ast.parse("'foo' < 'bar' <= 'baz' >= 'quux'"), annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE(), GtE()], [Str('bar'), Str('baz'), Str('quux')]))])"

And contrary to the currently accepted answer, the ternary operation is a generic comparison operation, that takes the first expression, an iterable of specific comparisons and an iterable of expression nodes to evaluate as necessary. Simple.

Conclusion on Python

I personally find the range semantics to be quite elegant, and most Python professionals I know would encourage the usage of the feature, instead of considering it damaging - the semantics are quite clearly stated in the well-reputed documentation (as noted above).

Note that code is read much more than it is written. Changes that improve the readability of code should be embraced, not discounted by raising generic specters of Fear, Uncertainty, and Doubt.

So why is x < y < z not commonly available in programming languages?

I think there are a confluence of reasons that center around the relative importance of the feature and the relative momentum/inertia of change allowed by the governors of the languages.

Similar questions can be asked about other more important language features

Why isn't multiple inheritance available in Java or C#? There is no good answer here to either question. Perhaps the developers were too lazy, as Bob Martin alleges, and the reasons given are merely excuses. And multiple inheritance is a pretty big topic in computer science. It is certainly more important than operator chaining.

Simple workarounds exist

Comparison operator chaining is elegant, but by no means as important as multiple inheritance. And just as Java and C# have interfaces as a workaround, so does every language for multiple comparisons - you simply chain the comparisons with boolean "and"s, which works easily enough.

Most languages are governed by committee

Most languages are evolving by committee (rather than having a sensible Benevolent Dictator For Life like Python has). And I speculate that this issue just hasn't seen enough support to make it out of its respective committees.

Can the languages that don't offer this feature change?

If a language allows x < y < z without the expected mathematical semantics, this would be a breaking change. If it didn't allow it in the first place, it would be almost trivial to add.

Breaking changes

Regarding the languages with breaking changes: we do update languages with breaking behavior changes - but users tend to not like this, especially users of features that may be broken. If a user is relying on the former behavior of x < y < z, they would likely loudly protest. And since most languages are governed by committee, I doubt we would get much political will to support such a change.

  • Honestly, I take no issue with the semantics provided by languages that chain comparison operations such as ` x < y < z ` but it is trivial for a developer to mentally map x < y < z to (x < y) && (y < z). Picking nits, the mental model for the chained comparison is general math. The classic comparison is not mathematics in general, but Boolean logic. x < y produces a binary answer {0}. y < z produces a binary answer {1}. {0} && {1} produces the descriptive answer. The logic is composed, not naively chained. – K. Alan Bates Apr 28 '16 at 15:49
  • To better communicate, I have prefaced the answer with a single sentence that directly summarizes the entire content. It's a long sentence, so I broke it up in bullets. – Aaron Hall Apr 28 '16 at 18:35
  • 2
    The key reason few languages implement this feature is that before Guido, nobody even thought about it. The languages that inherit from C can't get this "right" (mathematically right) now primarily because the developers of C got it "wrong" (mathematically wrong) over 40 years ago. There's lots of code out there that depends on the counterintuitive nature of how those languages interpret x<y<z. A language has once chance to get something like this right, and that one chance is at the language's inception. – David Hammen Apr 28 '16 at 18:51
  • 1
    @K.AlanBates You make 2 points: 1) operator chaining is sloppy and 2) that syntactic sugar has no value. To the first: I have demonstrated that operator chaining is 100% deterministic, have I not? Perhaps some programmers are too mentally lazy to expand their ability to comprehend the construct? To the second point: It sounds to me like you're directly arguing against readability? Isn't syntactic sugar usually considered a good thing when it improves readability? If it is normal to think in this way, why wouldn't a programmer want to communicate thusly? Code should be written to be read, no? – Aaron Hall Apr 28 '16 at 18:55
  • 2
    I have watched both Ruby and Python implement breaking changes. For those who are curious, here's a breaking change in C# 5.0 involving loop variables and closures: blogs.msdn.microsoft.com/ericlippert/2009/11/12/… – user2023861 Apr 28 '16 at 21:04
13

Computer languages try to define the smallest possible units and let you combine them. The smallest possible unit would be something like "x < y" which gives a boolean result.

You may ask for a ternary operator. An example would be x < y < z. Now what combinations of operators do we allow? Obviously x > y > z or x >= y >= z or x > y >= z or maybe x == y == z should be allowed. What about x < y > z ? x != y != z ? What does the last one mean, x != y and y != z or that all three are different?

Now argument promotions: In C or C++, arguments would be promoted to a common type. So what does x < y < z mean of x is double but y and z are long long int? All three promoted to double? Or y is taken as double once and as long long int the other time? What happens if in C++ one or both of the operators are overloaded?

And last, do you allow any number of operands? Like a < b > c < d > e < f > g ?

Well, it all gets very complicated. Now what I wouldn't mind is x < y < z producing a syntax error. Because the usefulness of it is small compared to the damage done to beginners who can't figure out what x < y < z actually does.

  • 4
    So, in short, it’s just a hard feature to design well. – Jon Purdy Apr 27 '16 at 21:46
  • 2
    This is not really a reason to explain why no well know language contains this feature. As a matter of fact, it's pretty easy to include it in a language in a well defined way. It's just a matter of viewing it as a list connected by operators of similar type instead of every operator being a binary one. The same can be done for sums x + y + z, with the only difference that that does not imply any semantical difference. So it's just that no well known language ever cared to do so. – cmaster Apr 27 '16 at 21:56
  • 1
    I think that in Python it's a bit of an optimization, (x < y < z being equivalent to ((x < y) and (y < z)) but with y only evaluated once) which I'd imagine compiled languages optimize their way around. "Because the usefulness of it is small compared to the damage done to beginners who can't figure out what x < y < z actually does." I do think it's incredibly useful. Probably gonna -1 for that... – Aaron Hall Apr 27 '16 at 22:09
  • If one's goal is to design a language that eliminates all things that might be confusing to the most foolish of programmers, such a language already exists: COBOL. I'd rather use python, myself, where one can indeed write a < b > c < d > e < f > g, with the "obvious" meaning (a < b) and (b > c) and (c < d) and (d > e) and (e < f) and (f > g). Just because you can write that does not mean you should. Eliminating such monstrosities is the purview of code review. On the other hand, writing 0 < x < 8 in python has the obvious (no scare quotes) meaning that x lies between 0 and 8, exclusive. – David Hammen Apr 28 '16 at 1:37
  • @DavidHammen, ironically, COBOL does indeed allow a < b < c – JoelFan May 3 '16 at 22:35
10

In many programming languages, x < y is a binary expression that accepts two operands and evaluates to a single boolean result. Therefore, if chaining multiple expressions, true < z and false < z won't make sense, and if those expressions successfully evaluate, they're likely to produce the wrong result.

It's much easier to think of x < y as a function call that takes two parameters and produces a single boolean result. In fact, that's how many languages implement it under the hood. It's composable, easily compilable, and it just works.

The x < y < z scenario is much more complicated. Now the compiler, in effect, has to fashion three functions: x < y, y < z, and the result of those two values anded together, all within the context of an arguably ambiguous language grammar.

Why did they do it the other way? Because it is unambiguous grammar, much easier to implement, and much easier to get correct.

  • 2
    If you're designing the language, you have the opportunity to make it the right result. – JesseTG Apr 27 '16 at 20:50
  • 2
    Of course it answers the question. If the question is really why, the answer is "because that's what the language designers chose." If you can come up with a better answer than that, go for it. Note that Gnasher essentially said exactly the same thing in the first paragraph of his answer. – Robert Harvey Apr 27 '16 at 21:26
  • 3
    Again, you're splitting hairs. Programmers tend to do that. "Do you want to take out the trash?" "No." "Will you take out the trash?" "Yes." – Robert Harvey Apr 27 '16 at 21:35
  • 2
    I also contest the last paragraph. Python supports chains comparisons, and its parser is LL(1). It's not necessarily hard to define and implement the semantics either: Python just says that e1 op1 e2 op2 e3 op3 ... is equivalent to e1 op e2 and e2 op2 e3 and ... except that each expression is only evaluated once. (BTW this simple rule has the confusing side effect that statements like a == b is True no longer have the intended effect.) – user7043 Apr 27 '16 at 22:32
  • 2
    @RobertHarvey re:answer This was where my mind immediately went as well for my comment on the main question. I don't consider support for x < y < z to add any specific value to language semantics. (x < y) && (y < z) is more broadly supported, is more explict, more expressive, more easily digested into its constituents, more composable, more logical, more easily refactored. – K. Alan Bates Apr 28 '16 at 16:48
6

Most mainstream languages are (at least partially) object-oriented. Fundamentally, the underlying principle of OO is that objects send messages to other objects (or themselves), and the receiver of that message has complete control over how to respond to that message.

Now, let's see how we would implement something like

a < b < c

We could evaluate it strictly left-to-right (left-associative):

a.__lt__(b).__lt__(c)

But now we call __lt__ on the result of a.__lt__(b), which is a Boolean. That makes no sense.

Let's try right-associative:

a.__lt__(b.__lt__(c))

Nah, that doesn't make sense either. Now, we have a < (something that's a Boolean).

Okay, what about treating it as syntactic sugar. Let's make a chain of n < comparisons send an n-1-ary message. This could mean, we send the message __lt__ to a, passing b and c as arguments:

a.__lt__(b, c)

Okay, that works, but there is a strange asymmetry here: a gets to decide whether it is less than b. But b doesn't get to decide whether it is less than c , instead that decision is also made by a.

What about interpreting it as an n-ary message send to this?

this.__lt__(a, b, c)

Finally! This can work. It means, however, that the ordering of objects is no longer a property of the object (e.g. whether a is less than b is neither a property of a nor of b) but instead a property of the context (i.e. this).

From a mainstream standpoint that seems weird. However, e.g. in Haskell, that's normal. There can be multiple different implementations of the Ord typeclass, for example, and whether or not a is less than b, depends on which typeclass instance happens to be in scope.

But actually, it is not that weird at all! Both Java (Comparator) and .NET (IComparer) have interfaces that allow you to inject your own ordering relation into e.g. sorting algorithms. Thus, they fully acknowledge that an ordering is not something that is fixed to a type but instead depends on context.

A far as I know, there are currently no languages that perform such a translation. There is a precedence, however: both Ioke and Seph have what their designer calls "trinary operators" – operators which are syntactically binary, but semantically ternary. In particular,

a = b

is not interpreted as sending the message = to a passing b as argument, but rather as sending the message = to the "current Ground" (a concept similar but not identical to this) passing a and b as arguments. So, a = b is interpreted as

=(a, b)

and not

a =(b)

This could easily be generalized to n-ary operators.

Note that this is really peculiar to OO languages. In OO, we always have one single object which is ultimately responsible for interpreting a message send, and as we have seen, it is not immediately obvious for something like a < b < c which object that should be.

This doesn't apply to procedural or functional languages though. For example, in Scheme, Common Lisp, and Clojure, the < function is n-ary, and can be called with an arbitrary number of arguments.

In particular, < does not mean "less than", rather these functions are interpreted slightly differently:

(<  a b c d) ; the sequence a, b, c, d is monotonically increasing
(>  a b c d) ; the sequence a, b, c, d is monotonically decreasing
(<= a b c d) ; the sequence a, b, c, d is monotonically non-decreasing
(>= a b c d) ; the sequence a, b, c, d is monotonically non-increasing
3

It's simply because the language designers didn't think of it or didn't think it was a good idea. Python does it as you described with a simple (almost) LL(1) grammar.

  • 1
    This will still parse with a normal grammar in pretty much any mainstream language; it just won't be understood correctly for the reason @RobertHarvey gave. – Mason Wheeler Apr 27 '16 at 21:02
  • @MasonWheeler No, not necessarily. If the rules are written so that the comparisons are interchangeable with other operators (say, because they have the same precedence), then you won't get the right behaviour. The fact that Python is putting all of the comparisons on one level is what allows it to then treat the sequence as a conjunction. – Neil G Apr 27 '16 at 21:04
  • 1
    Not really. Put 1 < 2 < 3 into Java or C# and you don't have a problem with operator precedence; you have a problem with invalid types. The issue is that this will still parse exactly as you wrote it, but you need special-case logic in the compiler to turn it from a sequence of individual comparisons to a chained comparison. – Mason Wheeler Apr 27 '16 at 21:18
  • 2
    @MasonWheeler What I'm saying is that the language has to be designed for it to work. One part of that is getting the grammar right. (If the rules are written so that the comparisons are interchangeable with other operators, say, because they have the same precedence, then you won't get the right behaviour.) Another part of that is interpreting the AST as a conjunction, which C++ doesn't do. The main point of my answer is that it's a language designer's decision. – Neil G Apr 27 '16 at 21:18
  • @MasonWheeler I think we agree. I was just highlighting that it's not hard to get the grammar right for this. It's just a matter of deciding in advance that you want it to work this way. – Neil G Apr 27 '16 at 21:19
2

The following C++ program compiles with nary a peep from clang, even with warnings set to the the highest possible level (-Weverything):

#include <iostream>
int main () { std::cout << (1 < 3 < 2) << '\n'; }

The gnu compiler suite on the other hand nicely warns me that comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses].

So, my question is this: why is x < y < z not commonly available in programming languages, with the expected semantics?

The answer is simple: Backwards compatibility. There is a vast amount of code out in the wild that use the equivalent of 1<3<2 and expect the result to be true-ish.

A language designer has but one chance at getting this "right", and that is the point in time the language is first designed. Get it "wrong" initially means that other programmers will rather quickly take advantage of that "wrong" behavior. Getting it "right" the second time around will break that existing code base.

  • +1 because this program outputs '1' as the result of an expression that is obviously false in mathematics. Although it's contrived, a real-world example with incomprehensible variable names would become a debugging nightmare if this language feature was added. – Seth Battin Apr 28 '16 at 13:34
  • @SethBattin -- This is not a debugging nightmare in Python. The only problem in Python is if x == y is True : ..., My opinion: People who write that kind of code deserve to be subjected to some extra-special, extraordinary kind of torture that (if he was alive now) would make Torquemada himself faint. – David Hammen Apr 28 '16 at 15:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.