Before getting into the question posed in the title, lets look at mathematical notation so everyone is up to speed. The more common way to use the notation in the title is -2 < x < 0
, this is especially common when a function changes behavior in different ranges. We could for example define the step function as:
$$
\text{step}(x) = \begin{cases}
1 &\quad \text{if} \ -1 < x < 1, \\
0 &\quad \text{otherwise}
\end{cases}
$$
Drawing the function, looks like this:
How to Interpret -2 < -1 < 0
With that short background, let us look at -2 < -1 < 0
. Given how we used this in the notation above, this has to result in a boolean value, so it is either true or false.
This question sounds like a trivial one. To the point of most non-programmer would give a look questioning your sanity, of course it must be a true statment. However, asking a C++ programmer they would answer that the expression is false, assuming this is a trick question. And in this post I explain why they have this reaction.
Python and Chained Comparison
This post started its life as a post about how, many times, mathematical notation maps really well to Python code. In the end, the most interesting part in that original post was a feature called chained comparisons. Chained comparison allow us to do this in Python:
>>> -2 < -1 < 0
True
As we would expect from a mathematical view, the expression returns true. But as I have hinted about, and as you might know if your mainly write code in other languages this is not always the case. Actually, most languages doesn't behave like this (which is the reasons it's a feature with a name). For this post I went on a quest for finding one more language which, as Python, supports chained comparison. During this journey, I found three main categories for how languages handles this expression; correctly, lying and just plain panic. I tested the following languages: C, C++, C#, Java, Kotlin, Octave (~Matlab), JavaScript, Elixir, R, Rust, Ruby, Go, Lua and Julia.
Category 1 - Lying
Let's start off by explaining my little joke about the C++ programer in the introduction. In C++, as well as in C, the answer to the expression is false. Technically, it is 0 but this is the same as false, sooo moving on. Two more of the languages I tested gave this response. These two were Elixir and JavaScript (JS), both gave false instead of 0, which I guess is a kind of improvement. I don't have much to say about Elixir, but I am not surprised at all about finding JS in this category. In the end this is the result of implicit typecasting, something JS really, really loves to do.
So, what actually happens here? The expression is evaluated in two steps, one for each less-than (<) operator. First the expression -2 < -1
which is evaluated to true. However, in all these languages true and 1 are, if not the same, close enough to be casted to each other. Therefore, when we make the second comparison we get true < 0
which gets converted to 1 < 0
, which is false.
Casting between boolean and the integer values 0 and 1 is really common. Actually, Python does this as well as it has bool implemented as a subclass of integer. If Python didn't support chained comparison, it would actually be in this category as well. We can confirm this by forcing the evaluation order:
>>> (-2 < -1) < 0
False
All of the languages covered so far are, what I would call generalist programming languages. But come on, a language like Matlab which is specialized in mathematics must handle this expression correctly. It being in this category might have given it away, but no, no it doesn't. Well, in this case I couldn't actually confirm it my self as I don't have a licens, but this post on the matlab forum suggests it doesn't. I did try it out in Octave, a FOSS project which according to the website has a syntax that is largely compatible with Matlab. In Octave, it evaluates to 0 (not even false...) and I would be surprised if the behavior differs in Matlab.
Category 2 - Panicking
Panicking is when the language screams at you when trying to evaluate the expression. Jokes aside, I believe this to be a huge improvement from the last category, at least these ones are honest. This is also the category that contains the largest amount of the languages I tried. I have split it into three sub-groups, depending on what it screams at you.
The first sub-group is, and I am paraphrasing a bit here: "No you can not compare a boolean to an integer using the less-than operator". This goes through a similar processor evaluation as described in the last section. However, they do not implicitly cast the result of the true-value from first evaluation step to a 1. Instead, they raise an error as you try to compare the first result, a boolean, to an integer unsg the less than operator; something you aren't allowed to do. In this sub-group we have Java, it's younger sibling Kotlin and cousin C#. Yeah, I am not very surprised that all of these are in the same group. Although, I was hoping for Kotlin to have broken free. We also have GO and Lua in this sub-group.
The second sub-group is: "Why are you putting a less-than operator there?". These languages gets confused by the second less-than operator. In this sub-group, we have R, which I thought was a strong candidate for supporting chained comparisons as it is focused on statistics. The error you get in this case is a syntax error as the second less-than operator is, and I quote, "unexpected". We also find ruby in this sub-group, however, the error is quite different. A boolean value in ruby doesn't support the less-than operator, which is the reason for rubys confused answer: "undefined method '<' for true:TrueClass".
The third and final sub-group is: "I know what you want, I just can't do it". This is the home of a lonely fellow named, Rust. Rust does sort of support chaining comparison and, therefore, gives an error message stating you need to add parentheses for chained comparisons to be valid. However, with an expression like my example, it seems like you won't be able to get the answer I am looking for. Adding the parentheses will set the evaluation order and in the end, this will result in comparing a bool to an integer. Rust is a very strict language and comparing a bool to an integer isn't allowed, resulting in the same behavior as e.g. Java. This sort of puts Rust the first sub-group. However, I still think it deserves its own sub-group for at least trying.
Category 3 - Correctly
Here we, of course, have Python which started this journey. After searching far and wide I actually find one more language in this category, Julia. Julia was designed for high-performance computing, this was a reason I thought it was a good candidate for actually including chained comparisons. In both languages the behavior of -2 < -1 < 0
is the same as -2 < -1 and -1 < 0
(in Julia you would swap and to &&). Both languages also support chaining an arbitrary amount of comparisons, although I wouldn't expect you to need much more than two operators. The most common use-case I can think of is for checking a variable is within a certain range, e.g -1 < x < 1
. Another nice thing with chained comparisons is how it only evaluates each value once. Try this in Python:
>>> def test():
... print("test")
... return 1
...
>>> 0 < test() and test() < 2
test
test
True
>>> 0 < test() < 2
test
True
After doing some research into Julia the same seems to be the case there as well. Not a big optimization as you could easily just store the value in a variable to skip the re-evalution, but it's nice.
Conclusions
It was actually quite fun to search for another language supporting chained comparisons. I am extremely grateful for having access to online environments for all these languages. I would not have done this if I hade to install each one on my own system.
In the end, is this a feature that we actually need? No, not really but it feels more natural, especially when you are new to programming. The lying languages are kind of scary in that sense, evaluating the expression without an error and produces a unexpected result. This can really trip up people new to programming, causing some hard to find bugs. I also think chained comparison increases the general readability of the code. However, I will note that the use-cases are quite few.
Finally, with this post I also want highlight how something which might seem trivial not always is. Making assumptions about how something "must work" is always a risk and when facing bugs you must be able to put your assumptions aside. -2 < -1 < 0
, isn't always a true statement.
Appendix - The step function
Here is the step-function defined in python using chained comparisons:
def step(x):
return 1 if -1 < x < 1 else 0
Now, isn't that beautiful?