43

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?

Aaron Hall
  • 5,895
  • 4
  • 25
  • 47
JesseTG
  • 647
  • 5
  • 13
  • 1
    Here's the grammar file, which they handily stick right in the Python documentation - I don't think it's that difficult: https://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 – David Hammen Apr 28 '16 at 01:07
  • I now specifically address the currently accepted answer with Python's own Abstract Syntax Tree demonstrating that it is wrong, and specifically address the question you raised. Cheers! – Aaron Hall Apr 28 '16 at 03:50
  • @JesseTG "exactly as you would expect" is a bit much, people sometimes get confused when they mix different operators (like `x in y == True` means `(x in y) and (y == True)`) – RemcoGerlich Apr 28 '16 at 07:14
  • 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(this T candidate, T lower, T upper) where T : IComparable` if it really bothers you to see `&&`s) – sara Apr 28 '16 at 14:54
  • 1
    Why, you ask? Because not enough people want it. Your `x < y < z` is more expressively representable as `(x < y) && (y < z)` Being able to chain these comparison operations for an in-place evaluation adds very limited overall value and I want the language teams to be adding useful features rather than syntactic sugar that I consider meaningless. – K. Alan Bates Apr 28 '16 at 15:40
  • @RemcoGerlich I don't know of any language that supports that structure, but in any language that **could** support it, I would expect `x in y == True` to represent an existential quantifier. `"If the check of x in y evaluates to true, branch"` – K. Alan Bates Apr 28 '16 at 16:51
  • @K.AlanBates: yes, but Python sees it as chained operators instead, so it can be confusing. – RemcoGerlich Apr 28 '16 at 18:01
  • @RemcoGerlich ...I'll take your word that Python allows this structure. I've never had any use for the language thus have never had a need to learn it (although that may change shortly) ...How can `x` be *in* `True`? That makes extremely little sense unless you're expecting this to do an implicit set expansion to contain one element `True` and expect the `in` check to be of the set containing `True` Is this what occurs? If not, it's flat out wrongheaded. If so, then holy crap that's convoluted. – K. Alan Bates Apr 28 '16 at 18:25
  • 2
    SQL is quite "mainstream" and you can write "x between 1 and 10" – JoelFan May 03 '16 at 22:36
  • 2
    [Raku](https://docs.raku.org/language/operators#Chaining_binary_precedence) supports this, and Perl v5.32 will support this. – brian d foy Mar 27 '20 at 05:39
  • Not only does Raku support this, you can create your own chain operators. `sub infix:« OP » (…) is assoc {…}` The runtime will also delay executing the inner expressions until they are needed. So for `1 < 1 < say(3)` it will never actually run `say(3)` – Brad Gilbert Mar 31 '20 at 19:40
  • Funny is on the other hand, that often there is _one_ ternary operation `cond ? then-value : else-value` for an if-then-else expression. And usually just named _ternary expression/operator_, though _ternary_ relates to 3, as _binary_ to 2. – Joop Eggen Apr 23 '20 at 11:12

10 Answers10

43

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.

To quote James Gosling, who gives no further explanation:

JAVA omits many rarely used, poorly understood, confusing features of C++ that in our experience bring more grief than benefit. This primarily consists of operator overloading (although it does have method overloading), multiple inheritance, and extensive automatic coercions.

And these words attributed to Chris Brumme, after citing the amount of work to determine the right way to do it, user complexity, and difficulties in implementing:

It's not at all clear that this feature would pay for itself. It's something we are often asked about. It's something we haven't done due diligence on. But my gut tells me that, after we've done a deep examination, we'll still decide to leave the feature unimplemented.

These aren't great answers. Python has had multiple inheritance for a long time, it's well studied - it seems to me these are just implementations that need working out now. Is the conclusion right for the language? Maybe. It does limit the expressiveness of the languages, though.

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.

Aaron Hall
  • 5,895
  • 4
  • 25
  • 47
  • 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
  • @AaronHall More arguably, the laziness is on the developer who prefers the transitive, "squishy", "implicit" logical chain rather than the declarative, expressive, explicit logical group. What you call "elegance" another may consider sloppy. The "value" added to language users that you reference is nothing more than syntactic sugar. Sugar is fine in a language if it provides noticeable improvements to terseness without compromising expressiveness. I say that the "improvements" to the suggested syntax are debatable and smacks of code I would expect to see in source of a one man team. – K. Alan Bates Apr 28 '16 at 18:46
  • 4
    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 – David Hammen Apr 28 '16 at 18:51
  • 3
    @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
  • @AaronHall I already provided in a comment above that I do not take issue with the chained operation style but I do not prefer it. I prefer code more closely aligned with fully fleshed out logical structures than "yeah. Take a guess that that code does what I meant for it to do." For a naive interval range check, the difference seems to me very trivial. But it seems useless to me. Languages already have that structure. It is represented by `(x < y) && (y < z)` In languages that support `x < y < z`, "Cool?? ...I guess." But it adds zero value. – K. Alan Bates Apr 28 '16 at 19:27
  • @AaronHall I did not say that operator chaining is sloppy; you were the one that declared it to be "elegant" and lack of support for it to be "laziness." What I said was that the elegance is debatable and the mark of laziness is much easier placed on the person who needs to see `x < y < z` instead of `(x < y) && (y < z)` than on the language team that considered your preferred expression style to be of arguably trivial value. – K. Alan Bates Apr 28 '16 at 19:35
  • 3
    `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: https://blogs.msdn.microsoft.com/ericlippert/2009/11/12/closing-over-the-loop-variable-considered-harmful/ – user2023861 Apr 28 '16 at 21:04
  • 3
    "Why isn't multiple inheritance available in Java or C#? There is no good answer here to either question" - disagree, the designers of Java deliberately excluded multiple inheritance and have spoken publicly about why. – JacquesB Apr 23 '20 at 09:22
  • @JacquesB can you provide a link? – Aaron Hall Apr 23 '20 at 14:54
  • @AaronHall: For example https://pdfs.semanticscholar.org/ee07/8894734c70589cb9653e2169a4342ae02355.pdf – JacquesB Apr 23 '20 at 15:37
  • @JacquesB I quoted what your link has to say about multiple inheritance but I don't see how my conclusion changes based on that link. Perhaps you have a mailing list discussion thread on it? – Aaron Hall Apr 23 '20 at 16:06
  • @AaronHall Clearly it was a deliberate design choice based on experience from C++, not "laziness" which is a ridiculous claim. The same is the case for C#, see for example https://docs.microsoft.com/en-us/archive/blogs/csharpfaq/why-doesnt-c-support-multiple-inheritance I'm sure you can find more eleboration by Gosling also. – JacquesB Apr 23 '20 at 16:24
  • I have added an additional quote - thanks for your contributions here. Now, "laziness" is used half in jest (we programmers are known for our "haha - just serious" ways of communicating.) Nevertheless, I think he's got a point - they looked at the cost in time and energy, looked at the benefits (as they were being sold on the idea) and decided it wasn't worth it. Is that lazy? Isn't laziness, to programmers, a good thing? One of the reasons I appreciate moving from Florida to NYC is that I no longer have to wash a car or mow grass. – Aaron Hall Apr 23 '20 at 17:12
  • @AaronHall: In a language where the result of `<` could not be used as an operand for `<`, efficient processing of `x < y < z` would be easier than efficient processing of `(x < y) && (y < z)`, especially in the common cases where `x` and `z` are constants. Such cases may be handled, when `z` is a constant larger than `x+1`, as `(y-(x+1u)) < (z-2u-x)`, thus allowing two compare-and-branch operations to be replaced with a subtract and a compare. – supercat Apr 23 '20 at 20:33
40

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.

Karl Bielefeldt
  • 146,727
  • 38
  • 279
  • 479
  • 3
    "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: https://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
  • 3
    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](http://www.cprogramming.com/reference/operators/ternary-operator.html) available in a lot of languages? – JensG Apr 28 '16 at 14:23
  • 2
    @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
  • @AaronHall: Agree on the FUD part. At the end, a construct like `a OP1 b OP2 c` would be just syntactic sugar for `(a OP1 b) AND (b OP2 c)` so I don't see the problem either. And yes, I often asked myself the same question as the OP. – JensG Apr 28 '16 at 14:46
  • I've edited to use the more familiar term. Most languages have *one* ternary operator, and it's symbols are not overloaded with binary symbols. Adding others would require adding a bunch of additional logic. Again, I'm not saying it's not possible, or that it's some big super-scary thing. However, it *is* extra work for a relatively minor use case. – Karl Bielefeldt Apr 28 '16 at 15:34
  • 7
    I'm one of the downvoters on this accepted answer. (@JesseTG: Please unaccept this answer.) This question confuses what `x – David Hammen Apr 28 '16 at 18:41
  • Another thing to consider is that some languages (e.g Haskell) don't even treat `<` as anything more than an infix function. So this kind of syntactic sugar would not be practical to implement. As you can change the meaning and type signature and even precedence of `<` arbitrarily. Or even assign another symbol to do the exact same thing. It would also be very unidiomatic (not in general, but in Haskell it would be). – semicolon Apr 28 '16 at 22:22
  • 2
    @david, the individual comparisons are indeed still binary, but to the *parser* it is ternary. It can't just magically desugar it without code to back that up. It has to be treated as ternary first. – Karl Bielefeldt Apr 28 '16 at 22:39
  • 1
    "You're basically creating a symbol that sometimes acts like a binary operator and sometimes effectively acts like a ternary operator" - in Python a comparison chain can be arbitrary long, not just two or three operands. – JacquesB Apr 24 '20 at 14:16
14

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.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
  • 5
    So, in short, it’s just a hard feature to design well. – Jon Purdy Apr 27 '16 at 21:46
  • 3
    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 - reinstate monica 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 01:37
  • 1
    @DavidHammen, ironically, COBOL does indeed allow a < b < c – JoelFan May 03 '16 at 22:35
11

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.

Robert Harvey
  • 198,589
  • 55
  • 464
  • 673
  • 2
    If you're designing the language, you have the opportunity to make it the *right* result. – JesseTG Apr 27 '16 at 20:50
  • This doesn't answer the question. – Neil G Apr 27 '16 at 21:01
  • 3
    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](http://programmers.stackexchange.com/a/316978/1204). – Robert Harvey Apr 27 '16 at 21:26
  • @JesseTG: There comes a point at which the added complexity is no longer worth it. – Robert Harvey Apr 27 '16 at 21:32
  • @RobertHarvey that's another good answer to why…  these are the things that should be in your answer. Read the question again, and then read your answer. – Neil G Apr 27 '16 at 21:34
  • This is the question: "So, my question is this: why is x < y < z not commonly available in programming languages, with the expected semantics?" – Neil G Apr 27 '16 at 21:34
  • @NeilG: And I've provided a plausible explanation. – Robert Harvey Apr 27 '16 at 21:34
  • No. You've just said what they do instead. – Neil G Apr 27 '16 at 21:34
  • 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
  • It's like if someone said: Why is california warm in the winter. And you said "california has an average temperature of 80° in the winter. Do you not see how that doesn't answer the question even though it's related to the question? – Neil G Apr 27 '16 at 21:36
  • 1
    @NeilG That's a straw man, and not even a good one. My answer is *not* a tautology; read it again. – Robert Harvey Apr 27 '16 at 21:36
  • I wouldn't call the implementation n-ary comparison operator "much more complicated". After all, it's just a matter of handling a list of arguments along with a list of comparisons, which can be done in a very straightforward fashion. – cmaster - reinstate monica Apr 27 '16 at 22:09
  • 4
    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.) –  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
8

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
Jörg W Mittag
  • 101,921
  • 24
  • 218
  • 318
4

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.

Neil G
  • 438
  • 2
  • 14
  • 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
  • 2
    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
  • 1
    @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
4

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.

David Hammen
  • 8,194
  • 28
  • 37
  • 1
    +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
1

The short answer: Because C did not have it.

The majority of today's mainstream languages have inherited the set of operators and the rules for operator precedence from C. This is the case for C++, Java, C#, JavaScript and many others.

Python, on the other hand, is not directly derived from C syntax. And several other non-C-derived languages support comparison chaining similar to Python, for example Perl, Raku and Julia. SQL supports a limited version with the between operator.

Of course this just raises the question of why C didn't have this syntax. I don't know if the designers of C even considered the syntax, but I doubt it. x > y > z would basically be syntactic sugar over x > y & y > z and would compile down to the same machine code. C was designed to be minimal and doesn't generally have a lot of syntactic sugar. (It might seem that way for e.g. the x++ operator which is equivalent to x+=1, but processors tend to have dedicated instructions for increment which is faster than a general addition, so a distinct operator is justified in this case.)

But the descendant languages like C++ or C# does not have the same focus on minimalism and could have added the syntax along the way. And a bit of googling show it has indeed been considered by several languages:

C#: Proposal: Chained Comparisons #2954 (https://github.com/dotnet/csharplang/issues/2954).

C++ Proposal: Chaining Comparisons (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0893r0.html)

I'm sure there are others.

The discussions show there are no fundamental objection against the feature, it is just a question if the benefit outweighs the cost of adding a new feature. For C++ there is also the issue of backwards compatibility, since x > y > z is already a legal expression.


I also want to add a non-reason: Some other answers claim that it is somehow especially complicated to parse an expression with three or more operands. This is not an explanation. All parsers, even for a simple language like C, can parse a function call with an argument list of arbitrary length no problem.

JacquesB
  • 57,310
  • 21
  • 127
  • 176
0

Stated simply: x < y < z will be interpreted either as (x < y) < z or x < (y < z).

In either case, the (parenthesized subexpression) will be evaluated first and will produce a boolean result: "either it is less or it isn't."

Leaving either: (boolean) < z or x < (boolean) ... both of which would be considered wrong.

The trivially equivalent "right way" works just as well, so that is how we say it: (x < y) && (y < z)

Mike Robinson
  • 1,765
  • 4
  • 10
0

In languages with operator overloading, this is possible.

So to see what it would be like, I implemented this in Swift:

precedencegroup ComparisonPrecedence {
    associativity: left
    higherThan: AdditionPrecedence
}

infix operator < : ComparisonPrecedence

// Start the chain, (C, C) -> ComparableWrapper<C>
func < <C: Comparable>(lhs: C, rhs: C) -> ComparableWrapper<C> {
    return ComparableWrapper<C>.startChain(lhs: lhs, rhs: rhs)
}

// Extend the chain, (ComparableWrapper<C>, C) -> ComparableWrapper<C>
func < <C: Comparable>(lhs: ComparableWrapper<C>, rhs: C) -> ComparableWrapper<C> {
    return ComparableWrapper<C>.extendChain(lhs: lhs, rhs: rhs)
}

// Terminate the chain, (ComparableWrapper<C>, C) -> Bool
func < <C: Comparable>(lhs: ComparableWrapper<C>, rhs: C) -> Bool {
    return ComparableWrapper<C>.terminateChain(lhs: lhs, rhs: rhs)
}

// A wrapper object which represents a node in a left-associative chain of `a < b < ... < z`
// Evaluating from left-to-right, it stores the largest value seen so far, and whether or not
// the elements so far have been strictly-increasing
struct ComparableWrapper<C: Comparable> {
    // The largest value in the chain so far, unless isTrueSoFar is false,
    // in which case it won't be needed anymore, so it's some arbitrary value in the chain,
    // to prevent needless further comparisons
    let value: C
    let isTrueSoFar: Bool   

    init(largerValue value: C, isTrueSoFar: Bool) {
        self.value = value
        self.isTrueSoFar = isTrueSoFar
    }

    static func startChain(lhs: C, rhs: C) -> ComparableWrapper<C> {
        if lhs < rhs {
            return ComparableWrapper(largerValue: rhs, isTrueSoFar: true)
        }
        else {
            return ComparableWrapper(largerValue: lhs, isTrueSoFar: false)
        }
    }

    static func extendChain(lhs: ComparableWrapper<C>, rhs: C) -> ComparableWrapper<C> {
        if lhs.isTrueSoFar {
            let newLargestValueSoFar = max(lhs.value, rhs)
            return ComparableWrapper(largerValue: newLargestValueSoFar, isTrueSoFar: lhs.isTrueSoFar)
        }
        else {  
            // Don't even bother comparing, the result will be false eventually anway.
            return ComparableWrapper(largerValue: rhs, isTrueSoFar: false)
        }
    }

    static func terminateChain(lhs: ComparableWrapper<C>, rhs: C) -> Bool {
        return lhs.isTrueSoFar && lhs.value < rhs
    }
}



let x = 3

if 1 < 2 < x < 4 < 5 {
    print("true!")
}

let start = ComparableWrapper<Int>.startChain
let extend = ComparableWrapper<Int>.extendChain
let terminate = ComparableWrapper<Int>.terminateChain


let boolResult = terminate(extend(extend(start(1, 2), 3), 4), 5)
print(boolResult)

I define a new infix < operator that shadows the built in one. Rather than it having just one type, (Value, Value) -> Bool, I define 3 separate overloads for it. I make the 3 overloads call 3 differently named methods, so as to be able to talk about them unambiguously.

The above code parses like:

terminate(extend(extend(start(1, 2), 3), 4), 5)

There are several problems with this code:

  1. If the standard library were to do this, 1 < 2 would become ambiguous without context. The result could either be Bool or ComparisonWrapper<Int>.

    • It could parse as the regular comparison operator (Int, Int) -> Bool
    • Or it could parse as startChain(1, 2), a function of type (Int, Int) -> ComparisonWrapper<Int>

    Special rules could be introduced to disambiguate it, but adding special cases to a language always has trade-offs.

  2. This is already kind of hairy, and it only support <, not <=, >, >= and ==.

  3. It introduces a lot of overloads, which slows down type checking.
Alexander
  • 3,562
  • 1
  • 19
  • 24