Hacker News new | past | comments | ask | show | jobs | submit login
JavaScript: The Curious Case of null >= 0 (campvanilla.com)
185 points by beefhash on Sept 9, 2017 | hide | past | favorite | 141 comments



Not to defend JavaScript too much, but I think the three examples seem reasonable when viewed in the right way, and I think it's hard for JavaScript to do much better with its existing design philosophy (dynamic typing and coercion rather than runtime errors to deal with type mismatches).

>= and > are numerical operations, so both sides are interpreted as a number, and null is 0 when treated as a number. That means `null > 0` becomes `0 > 0`, which is false, and `null >= 0` becomes `0 >= 0`, which is true. In contrast, == is a much more general operation that works on numbers as well as objects, strings, and all sorts of other things, so it certainly doesn't make sense to always convert both sides to a number first. I'm certainly happy that `null == 0` is false in JavaScript, and I wouldn't want that changed to be consistent with numerical comparison (which comes up much more rarely than equality checks, at least in my code).

This type of issue isn't unique to JavaScript. In Java, if you have boxed Integers, `==` will do object identity comparison (so two different instances of the same number will compare as not equal), while `>=` etc will unbox them and do numerical comparison, which I think stems from the same issue of equality being more general than numerical comparison.

Also worth noting that it's not always true that `a >= b` means `!(a < b)` in JavaScript. `'a' >= null` and `'a' < null` are both false.


Let me agree with you but then turn that on its head.

You've argued that having `null >= 0` is the best that JS could do while having a coercion-based semantics. And I agree: this confusing behavior may be the best you can possibly do with a coercion-based semantics. And it's not just this example, either. JavaScript is filled with gotchas, and a lot of them have to do with automatic coercion. For example, did you know there are number and string values x, y, and z, such that x<y, y<z, and z<x?

But if this is really the best you can do in the presence of automatic coercion, maybe that's an argument against coercion.


It took me a bit to dig through the fringe cases to find the combination (or maybe an additional combination?) to pull that off.

x = ''

y = '-Infinity'

z = -1

x < y // true: '-Infinity' (y) doesn't get coerced into a number, so '' < 'any string with at least 1 character'

y < z // true: '-Infinity' (y) compares with a number, so it coerces to -Infinity, which is less than -1 (z)

z < x // true: '' (x) is compared with a number, so it coerces to 0, which is greater than -1 (z)

Thank you for the brain exercise!

P.S. - No one ever use crap like this in production code!


Oh interesting. The triple I found used the same general idea of values that sometimes coerce to a number, but it made use of octal values instead of Infinity.


>> there are number and string values x, y, and z, such that x<y, y<z, and z<x

Again though < and > are numerical operators as poster pointed out so comparing a number to a string using these operators is... 'nonsensical' if that's perhaps the right term.

Consider checking if object foo > 1. What would that mean?


That's the point. JavaScript lets you do nonsensical operations that are easily detectable as nonsensical, but instead of throwing an exception, it goes out of its way to prevent you from noticing that anything is off.


> having `null >= 0` is the best that JS could do while having a coercion-based semantics. And I agree

Well, I disagree. You have to add a third condition for this to be the best you can do, and that is that the boolean type has to be two-valued. But that is not a given. Just as numerical types can include infinities and NaNs, a boolean type could include a third value that is neither true nor false. The IF statement would have a third clause to handle this value, something like:

    IF condition
    THEN [code if condition is true]
    ELSE [code if condition is false]
    OTHERWISE [code if condition is neither true nor false]
Like ELSE, the OTHERWISE clause would be optional so this would be a backward-compatible change. By default, existing code that encountered non-true-non-false conditions would do nothing.


Are you seriously suggesting that, widgety though JS's implicit type conversions might be, if its booleans had three values then things would be better?


Booleans having only three values would be an improvement for JavaScript where they actually have four : true, false, null and undefined


This is nonsense. If typeof(x) is 'boolean', then x is either true or false.

Now, if you don't know the type of x and try to use it as boolean without being careful, it might look like it has four values. That comes partly from being dynamically typed and partly from performing coercion instead of throwing errors. So one might argue that Javascript encourages people to write code that's not careful, or that it's very clumsy to write careful code in Javascript, but saying booleans have four values is just nonsense.


It's perfectly okay for a Boolean algebra to have four values. The two extra values can't behave the way JavaScript's “null” and “undefined” do, though.

No three-valued Boolean algebra exists.


Why not? SQL has three valued logic. It's not a novel idea.


Indeed it does, and it's horribly error-prone because people don't actually understand it :/. It can also lead to quite convoluted query logic because you're giving up the "law of the excluded middle"[1] (amongst other things).

For my money, the only sane thing to do with e.g. "null >= 0" is to throw an exception. (Even better would be just rejecting it at compile time, but JS obviously doesn't have that luxury.)

[1] https://en.wikipedia.org/wiki/Null_(SQL)#Law_of_the_excluded...

EDIT: Ninja edit: I do understand that NaN has 'reasons', but ideally it'd really want NaN == x to trap rather than just doing 'the weird thing'.


Humans not taking the time to understand SQL properly is their own fault though. Three value logic works perfectly well in the right hands.

JavaScript is horrid :(


Re: "their own fault". In one sense, yes, that's true, but this type of reasoning irks me, because you're kind of assuming that the language design is inconsequential -- when it clearly isn't. I could also (somewhat absurdly) also argue that Malbolge[1] is a perfectly reasonable language. After all, if you fully understand Malbolge, it shouldn't be that difficult to write correct programs in it, right?

This is obviously a bit of an Argumentum ad Absurdum, but I think it makes the point pretty well? There are concrete, important differences between languages regarding error-proneness, etc. The reality is that humans are lazy, fallible, etc. and we will make random mistakes, so if we can prevent mistakes (or at least catch them early), then that may be a worthwhile trade-off. Especially for such a trivial case. When was the last time you actually needed "null >= 0" to be true, for example? :)

[1] https://en.wikipedia.org/wiki/Malbolge (pleasantly surprised to see that it has a Wiki entry)


Well said. You should write a blog post or something on this topic.


One thing that helps with three-value logic is to insist that all conditionals ultimately have to boil down to just true or false, even if they are using the third value in it. This is the case in C#, which implements three-value using nullable bools. So e.g. if you have this:

    bool? t = true;
    bool? f = false;
    bool? u = null; // "unknown"
Then you can do things like (t & u) or (f | u) or (!u), and it works as you'd expect. But you can't write:

    if (t & u) ...
for example, because "if" requires a definite true or false. So e.g. if you want it to execute only if your expression is definitely true, you do:

    if ((t & u) == true) ...
etc. Consequently, you don't get silent bugs because a null slips through somewhere - if your expression ends up introducing nulls at some point, you always have to decide what exactly it means for it to be null at the end of the pipeline.

Coincidentally, it also doesn't allow (t && u) or (f || u), for the same reason why it doesn't allow "if" - because the operators are short-circuited, and whether the second operand is evaluated or not has observable side effects, so null/unknown cannot be handled safely.

In SQL, on the other hand, the use of NULL is a conditional implicitly does what I did explicitly above - i.e. NULL is basically treated as FALSE - which IMO is wrong in principle, but more importantly, results in silent, hard to detect bugs.


I'm aware that SQL has "bit" values that can be 1/0/null, but that's not exactly three valued logic. Does it really have conditional checks that can have three possible outcomes, or something similar?


Yes, a boolean value can be checked for the three possible values, TRUE, FALSE or NULL.


Yes. Why would you doubt it? (i.e. why would you doubt that I'm seriously suggesting it, not why would you doubt that what I am suggesting has merit)

(Oh, and BTW, before you completely dismiss the idea that my suggestion might have merit, you might want to look me up. I didn't just fall off the turnip truck.)


> I didn't just fall off the turnip truck

Don't get me wrong - I don't know who you are but your username is in my mental bucket of "people worth paying attention to" just from past comments. I ask because the idea of a three-valued boolean seems rather more "magical" than whatever sins JS already commits by defining comparisons such that (a<=b || b<=a) can be false, and so on. I mean, isn't it a contradiction in terms to begin with, to call such a beast a boolean?


It doesn't matter what you call it. What matters is that you somehow distinguish between situations that are clearly true or false and situations that are not clearly one or the other. The exact mechanism by which you do this doesn't really matter (and what you call it really doesn't matter). What matters is that you don't discard the potentially valuable information that you just tried to do an operation that doesn't make sense. Making null>=0 return TRUE is like making 0/0 return 1.


Can you provide an example of when this would be useful? For example:

   if (foo.bar.length >= baz.buz) {
      ...
   } otherwise {
      // what am I supposed to do here?
   }
How do I disambiguate if the "broken" type coercion was actually what I wanted? How do I tell what circumstances conspired to make this statement not-clearly-true?


You'll have to provide some more details. Why would you want this broken type coercion? What are you actually trying to do here?

But in the absence of this information, my first guess would be:

   if (foo.bar.length >= baz.buz) {
      ...
   } otherwise {
      alert("Something unexpected happened.  Either foo.bar.length or baz.buz is not a number.")
   }


This sounds like throwing an error with extra steps.

Isn't what you're describing here basically equivalent to saying JS should throw on invalid comparisons, with

    try {if (x) then A else B} catch C
rewritten as

    if (x) then A else B otherwise C

?


Yes, except that the program would continue rather than halt if you left out the OTHERWISE clause. This seemed to me to be more in keeping with the philosophy of the rest of the language.


I'm trying to compare two completely arbitrary variables: the point is there's a dozen different ways where that comparison could involve type coercion. The "third boolean" value is useless unless you completely redesign the language.


> I'm trying to compare two completely arbitrary variables

But WHY are you trying to compare them? The fact that one of these variables is a field called LENGTH is highly suggestive of some particular semantics that you intend these variables to have.

> there's a dozen different ways where that comparison could involve type coercion

That's right, which is the reason I can't answer your question as you posed it.

I predict that if you try to fill in the details what you will find is that you will be unable to do so in any way that does not reveal the presence of something badly wrong in your design.

> The "third boolean" value is useless unless you completely redesign the language.

You have to add an OTHERWISE clause to the IF statement and change the semantics of some operators. That's not a "complete redesign."


It sounds like OTHERWISE is vaguely analogous to a catch on Java's ClassCastException (assuming it would be thrown for any nonsensical comparison). Traditonal if/else maps IF to true and ELSE to false|undefined; OTHERWISE simply extracts the undefined case to a separate clause.

Interesting way of expressing the result of an ill-defined operation. Or am I off base?


No, you're exactly right. If you think about it, both of these things are (almost) exactly equivalent. In both cases you are separating out a third control branch. The only difference is the syntax you use to specify what happens on that third branch, and whether or not that branch is taken immediately when the non-true-non-false value is generated. It is exactly the difference between (say)dividing by zero throwing an exception or returning an infinity or a NaN.


Exceptions are very much well-defined results. They just aren't the result you were originally expecting. But the meaning of a program is defined in terms of the language's own semantics, not what the programmer expects.


I am intrigued by your ideas and would like to subscribe to your newsletter.

Jokes aside, a 3 value Boolean conditional could have some useful applications.


FWIW, for the downvoters: you really should look 'lisper up. I'm not sure I agree with him but the incredulity shown here is unwise.


The down votes are likely for trying to invoke an ad hominem argument. No matter someone's credentials, it's still a logical fallacy and something to be avoided. If he is an authority on the subject, it should show through in his comments. I'm sure he's a very smart and knowledgable person. But there are a lot of smart and knowledgable people here and by invoking his credentials, he's presuming that the people he's talking to are less knowledgable, intelligent or entitled to their opinions. It's insulting and he's earned his downs.

Full disclosure: I downed his 'look me up' post and upped his more substantive posts.


> If he is an authority on the subject, it should show through in his comments.

The comment speculating on the "OTHERWISE" clause is the kind of brilliant off-beat thinking that challenges the level of understanding of the reader.

The people scoffing at and downvoting it are, however smart and knowledgeable they may be, ignorant or foolish. If they thought it through they might learn something.

It's a perfectly valid response to say, "Hey wait a minute, I know what I'm talking about here." It's not appeal to authority, and it is certainly not an ad hominem since the people he's talking to have shown that they don't understand.


> brilliant off-beat thinking

Those words must have new meaning since he was citing as an example a language that's existed since the 70s. Ternary booleans are far from a unique or novel concept. The main difference is that for most languages, including Javascript, that third value exists at the variable binding or expression level, not the value level. Other languages that don't allow nulls encode that third value in a Maybe type. SQL, being a thin abstraction over data storage that needs to be able to encode nulls, doesn't make that distinction. That his idea was poorly worded to state that the actual Javascript value would have a third possibility rather than the comparison expression being nullable doesn't add to his credibility. The distinction between an expression evaluation, variable binding and a value isn't exactly a newbie concept, but for someone with the credentials he's claiming, I'd expect him to know it and be more precise.

> since the people he's talking to have shown that they don't understand.

Perhaps you should go back and read the exchange. The comment he was replying to was specifically about the mixture of Javascript and ternary booleans, not just ternary booleans. Also, if you re-read the comment that you're calling ignorant in the context of the authors original confusing of expression evaluations and values there's at least the possibility that he's making a very valid point, albeit in a way that can be read as confrontational. Javascript could already add an otherwise block without adding a third boolean value...all that would be necessary is to allow comparison operators evaluate to null or undefined and then execute the otherwise block in that case. Adding a third value would mean that a boolean variable binding could now have 5 values, true, false, not_true_or_false, null or undefined. That's nonsensical and deserves to be called out. It's bad enough that the language has null and undefined, since it causes a ton of confusion that could have been avoided. Adding some special "not null but sort of null" boolean value would just add more confusion to the language.


Alright, you got me. I went back and re-read the thread and I misunderstood what lisper was saying. You're right. I thought the OTHERWISE clause would run for null or undefined, but lisper does mention a third Boolean value. Your points above convinced me that that wouldn't work well (at least in Javascript.)

I still disagree about the argument from authority, but I think that's subjective.

Warm regards.


> Adding a third value would mean that a boolean variable binding could now have 5 values, true, false, not_true_or_false, null or undefined. That's nonsensical and deserves to be called out.

FWIW, I actually agree with this. There's no need to add a new value. It would be fine with me (within the context of Javascript's already horribly broken design) to use an existing value (null or undefined) as the third "boolean" value.

The part that really matters is that whatever null>=0 returns it should be different from either 1>=0 or -1>=0.


I think we mostly agree, which is why I upped your original comment calling for an otherwise block, despite the unnecessary (or imprecise) call for a third boolean value. As a slightly more verbose option that didn't involve adding a new 'otherwise' keyword, it could also be implemented with a catch block with the comparison operator throwing an exception rather than silently coercing. I find both an improvement on than the current behavior.

I was merely explaining why I downed your other comment. I don't care if you're a fresh bootcamp grad or the former lead on AdWords...your argument is equally compelling. I'm calling it out because I think it discourages people from engaging in conversations where they can learn. If you are who you say you are and have the experience I think you do and someone significantly more junior disagrees with you, it's an opportunity for you to explain your thinking in a way that helps them improve theirs going forward. Instead, you took an expedient and, IMHO, intellectually lazy route. It's a small thing, but it's all part of making our field more welcoming to newcomers. What JavaScript beginner is going to argue with the ex-AdWords lead? Beyond being an appeal to authority, your comment possibly shut down someone's positive learning experience.

And if it seems like I'm quibbling with you or being overly hard on what you've said, that part is definitely about who you are. I feel like those of us who've been in the industry a while (I'm a few years behind you, but I'm still nearing my 20th year doing this stuff) have an obligation to be better about technical discussions and work towards positive conflict, even if the other side seems intent on going negative. It's hard and I fail at doing it constantly, so I say this with the full realization that I'm often the proverbial 'pot' to your 'kettle'.


It seems like this thread has gone several different directions. Surely suggesting that that (null>0) should return null or undefined is a separate topic from suggesting that if() statements have three different control branches?

FWIW, I totally buy the former. If (null>0) returned null, then most things would still work as expected, and you could easily check the result of the comparison to catch unintended behavior. One can easily imagine a world where JS was defined this way from the start, and it'd be pretty much the same language.

The latter (three-way IF control structures) strikes me as waaaay more tenuous - much bigger change, more complexity on the programmer, and no benefits I can see that you wouldn't get from the former bit.


The reason for the three-way IF would be to avoid conflating null and false. Without that you'd have to write:

    IF (x<y)==null {
      [otherwise]
    } ELSE IF (x<y) {
      [then]
    } ELSE {
      [else]
    }
That seems awkward to me.


Yeah, I get that that's the intent, but (per my original comment) I'd think it would surely create more mess than it cleans up.

For example, I suspect that three-way IFs like you describe would only really get used in two ways - often the second and third branches would be identical, and the rest of the time the third branch will be some kind of "console.warn('bad inputs!')" kind of error handling.

For the latter of those two cases, testing for the error case before proceeding to the regular logic (as in your code sample above) seems intuitively like the right thing to do - analogously to how you might check (isNaN(operand)) before doing some math. And in the former case, if the second and third branches are identical you'd almost certainly want some syntactic sugar to avoid writing the same logic twice, like "IF (bool) {...} ELSE-AND-OTHERWISE {...}" -- which would just be isomorphic to what we have now. You know what I mean?


Except 'lisper wasn't making an argument. He was responding to a case of somebody being socially jerkish in a way that was pretty gentle, even for HN's coddling standards. That's important, because downvotes don't fix jerks--only pointing out the behavior does.

Coming in hot with "oh but fallacies, hmm hmm, heaven forfend" when what's being suggested is "maybe don't be a jerk?" is not a great look, don't you think?


He was making an argument and its disingenuous for you to suggest otherwise.

Go back and look at what he wrote. He basically said, "before you dismiss my idea as not having merit, go look me up." Ideas have merit independent of who has them. That's the point. He was citing his credentials to back up his earlier argument.

I'm not saying that the comment that he was replying to was right or even civil and had he left off his second "Oh, BTW..." line, it would have been a reasonable response. But including it not only speaks to the comment he was replying to but also to everyone who disagrees with his idea.

BTW...parodying what I said followed by condescension is also not a great look.


I'm sorry but I think your pattern matching for "Argument from Authority" is misfiring here.

Lisper wasn't saying, "I'm right because I'm me."

He was saying, "Adjust your prior[1] for my statement given additional data about its source."

[1] https://en.wikipedia.org/wiki/Prior_probability


Quoting directly:

> Oh, and BTW, before you completely dismiss the idea that my suggestion might have merit, you might want to look me up.

He wasn't saying "I'm right because I'm me." He was saying, "I'm me so don't assume I'm wrong." It's still an appeal to authority. Had his credentials been part of the first line, where he was confirming that he was serious about his previous post, then your interpretation would be correct.


> Had his credentials been part of the first line, where he was confirming that he was serious about his previous post, then your interpretation would be correct.

Wow, you have quite the exacting standards.


Hey...context matters. When you're talking about your own tendencies (whether you're likely to be serious or joking), who you are matters. When you're supporting your argument it doesn't.

If you call that exacting standards, so be it. I call it reading comprehension.


> Hey...context matters

Indeed it does. So let's recall the context in which I made my original comment:

> Are you seriously suggesting that, widgety though JS's implicit type conversions might be, if its booleans had three values then things would be better?

That seemed to me to be a pretty non-constructive question, essentially a passive-aggressive way of saying, "You can't possibly be serious. You must be a complete newbie dweeb to come up with such a dumb idea."

That's why I responded the way I did.


Smart people can still say dumb things.


Yes, they can indeed.


That's not a backward-compatible change. Branches that now take either the then or the else branch will take no branch instead. Obviously, that would change the meaning of many programs in ways the author didn't intend by not executing the code in those branches.


Yeah, I should have said it was syntactically backwards compatible. It's obviously not semantically backwards compatible.


Aggressive coercion in an attempt to execute clearly faulty code is exactly the problem.


The most common mistake I see when people talk about JS quirks is to assume that this kind of feature is always about fault tolerance rather than convenience.

For example, many languages treat conditions as "if nonzero". From a certain perspective, you could argue that

  if (i--) {
is "clearly faulty code".

From another perspective, you would say it's a shorthand whose meaning is obvious to anyone familiar with the rules of the language.

In my view, JavaScript's sin is that its rules are complicated, and it's difficult to statically analyze, so tools intended to guard against its pitfalls can only do so much.

Whether or not the complicated rules are about faulty code is pretty irrelevant. ASI is an error correction mechanism, but its pitfalls are easy to detect, so in modern environments it doesn't matter. The behavior of comparison operators seems to me like more of a convenience feature, but it's still a problem; the rules are easy to forget, and only a very draconian linter rule can protect you.


Honestly, I like implicit coercion of empty stuff into false. Yes, I do agree it makes the code more legible.

That said, objectively it doesn't gain that much. It may be that the cost of having any kind of implicit coercion is bigger than such gain. Implicit numeric coercion is another topic to think about, it is also hard to be sure if it is a net gain.


But not all code that looks faulty is faulty. I know plenty of competent people who have used the numeric properties of undefined and null directly with numeric operators knowing exactly what they're doing. Likewise there is a natural association between nonzero and true, zero and false, which is very useful (multiplying by a boolean, accumulating a list of booleans in a collection of records to get a count).

If you're using JavaScript, I encourage you to embrace the dynamic, coercive funky nature of the language. There is a twisted logic to each twisted feature.


But the code using twisted features is hard to maintain afterward.

You will have to put comments around them just to be sure the next guy (it might even be the author himself) doesn't waste its time understanding what the hell the code is doing.


> But the code using twisted features is hard to maintain afterward.

    let a = 0;
    for (const record of records)
      a += record.bool;
What's so bad about this? (aside from mutable accumulator, but that can be restricted to one scope). The pattern is basically directly lifted from C, where there's thankfully no such thing as a boolean type. If anything, the solution is to fold Boolean and null into Number to begin with.

> You will have to put comments around them just to be sure the next guy (it might even be the author himself) doesn't waste its time understanding what the hell the code is doing.

I sincerely hope that anyone who writes JavaScript on my team knows the coercion rules in JavaScript; otherwise they should probably be writing some transpiled language that lacks these features.


Non-zero does not imply `1'.

>The pattern is basically directly lifted from C, where there's thankfully no such thing as a boolean type.

There has been a boolean type in the current standard for C for longer than some of the younger people here have been alive.


Wow, I would never have known about stdbool. I have never seen it used (explicitly), even though it's been around since I was two years old. Thanks for bringing this to light! :- )

Nonetheless, bool or _Bool (aside from having implicit saturating arithmetic and assignment) is a one-bit integer type. It seems it mostly just formalizes the (implied, fictional) narrowing conversion involved in testing conditions, which is probably why I've not seen it used explicitly. The macros true and false seem to expand to 1 and 0 respectively.


That's because Boolean is a one-bit integer type. Whether you choose to call your 0s and 1s "false" and "true", correspondingly, doesn't change that.

Furthermore, from this perspective, a Boolean AND is just integer multiplication; Boolean OR is saturating addition; Boolean XOR is inequality; and Boolean IMP is saturating subtraction.


To be fair, I'd trade that for

    a += record.bool ? 1 : 0;
if I can have less awkward type coercions in return in a heartbeat.


Come on! Tongue in cheek

  a += !!record.bool;
There now. Happier?


This misses the point. !! stills returns a Boolean.


To be fair,

    let a = 0;
    for (const record of records) {
        if (record.bool){
          a += 1;
        }
    }
or

    ls.filter(x => x).length;
aren't that much harder to read. The second one probably doesn't do deforestation and would end up stupidly slow, though.


For the first one, would engines like v8 be smart enough to optimize it so it isn't wasting time on a pointless if-branch? (I'd imagine not, as it would be pretty hard for the engine to know that all the record.bool's are booleans.)


> Also worth noting that it's not always true that `a >= b` means `!(a < b)` in JavaScript. `'a' >= null` and `'a' < null` are both false.

For anyone wondering why (like me), it's because +'a' returns NaN, and comparisons with NaN always return false (NaN <= 0, NaN >= 0 and NaN === NaN are all false).

Specifically, the Abstract Relational Comparison algorithm for 'a' < null returns undefined (instead of true or false), and all comparison operators return false when it does that. [1]

When neither argument is NaN, what the author wrote is true and a >= b === !(a < b).

[1]: http://www.ecma-international.org/ecma-262/8.0/index.html#se...


>I think the three examples seem reasonable when viewed in the right way

These kinds of statements is why I think webdev is completely and thoroughly fucked. When obviously bad software or language design is widely accepted on the basis of convoluted technicalities you know that thing will not get better, only worse.

>This type of issue isn't unique to JavaScript. In Java, if you have boxed Integers

Boxed types in Java were a clusterfuck of their own. They shouldn't be used as justification of bad design in other languages. A cautionary tale, more like.


I think most people (including me) would agree that type coercion makes JavaScript a worse language, and that boxed types are one of the ugliest things in Java. By "the three examples seem reasonable when viewed in the right way", I mean that it's possible to build an intuition about how JavaScript behaves in these situations, and it doesn't require memorizing a giant spec. The behavior isn't random, it's the consequence of some early design decisions that likely should have been made differently, and understanding those design decisions will make you a better JavaScript programmer.

Many smart people have been working on improving JavaScript for a while now, and many of the problems have been fixed either by language improvements or widely-available tools. The runtime behavior of basic operators can't be changed, though, so we'll likely be stuck with the type coercion rules for a long time, although it's worth noting that both TypeScript and Flow disallow `null >= 0`.


>Also worth noting that it's not always true that `a >= b` means `!(a < b)` in JavaScript. `'a' >= null` and `'a' < null` are both false.

That also means that the concluding section of the article is simply wrong. To be honest, it's a pretty poor read – a waste of many words to explain a spec that's pretty straightforward itself, and ending up in a major misunderstanding.


Yet 1 == "1". & comparisons also apply to string/string not through numerical conversion


This being a result of JavaScript's weapons grade weak-typing, I've always wondered of what benefit weak typing actually brings, once dynamic typing is assumed, over strong typing as in python/Ruby. Or at least stronger typing. Reasonable coercion from 11434 -> "11434" is one thing, but why not throw an exception when anything more ambiguous is encountered? It would seem even for an absolute newcomer to programming, or someone experienced who wants to rapidly prototype, the bugs resulting from this hidden coercion complexity outweigh any gains in productivity. Especially considering the alternative is simply a set of special unambiguous casting functions that could be easily looked up.


> I've always wondered of what benefit weak typing actually brings

It makes your code crash less, and I would argue that in many cases that's desirable. I think it's a bit sad that the default behavior for computers is often to completely give up on the sight of any error. If you're running code in development or need to worry about data integrity, it makes sense to fail quickly and loudly, but if the analytics system or the "like" counter or some other non-critical feature crashes for a real user, it's a much better UX to degrade that feature rather than taking down the whole page.

I think the "avoid crashing at all costs" mindset was especially sensible when the web was viewed more as a collection of documents, with JS existing to give light optional enhancements rather than drive the core functionality. Imagine opening a Word doc or a PDF and having it crash because the author made a mistake. These days, I would certainly prefer that JS be more strict by default, especially in development.

I think the right way to handle the situation these days should be to define boundaries where a crash in one part of the code is contained to its boundary, e.g. React error boundaries ( https://facebook.github.io/react/blog/2017/07/26/error-handl... ). I also think it's useful to have a variant of "assert" that just logs a warning and continues in production, since in many cases that's the desirable behavior.


> It makes your code crash less

Wait, can you back this up? What makes you think strongly typed languages crash more than weakly typed languages? What's a scenario where a strongly typed language will crash at runtime, but a weakly typed language won't?

I can see an argument that an interpreter of a weakly typed language will let you run a program with dangerous type conversions without complaining, while a strongly typed language won't even let you execute it - but are you counting that as a crash?


To be clear, I'm talking about strong/weak typing and static/dynamic typing as distinct concepts. My comment mostly applies to dynamic-typed languages.

In JavaScript, `1 + {}` gives the string `1[object Object]`. In Python, `1 + {}` crashes. Neither language has a static type system, so neither language is able to disallow an expression like `a + b`; it needs to actually execute the code to find out that you're adding two things that don't make sense. I think most examples of "Python is stronger-typed than JavaScript" are of that form; Python crashes while JavaScript silently does something that may or may not make sense. Accessing a missing property, accessing an array out of bounds, and calling a function with the wrong number of arguments are also examples where Python crashes and JS doesn't.

So "crashing less" is pretty much inherent in my (simplified) definition of weak typing, and not meant to be anything controversial.


Why is crashing less often better in these cases? When it crashes, you know there is a problem with the code that needs to be fixed. When it doesn't, you might have nonsensical results that you're not aware of.


It depends on the exact context, but I think that in many cases, it's better to present wrong data to the user than to crash the page, since crashing the page makes it useless. Let's say Facebook accidentally introduces and ships some bug when computing the "like" count on about 1 in 5 news feed items. I could imagine two scenarios:

1.) The JS code crashes and the Facebook news feed page breaks for basically everyone. The flood of errors is reported to the error monitoring system and Facebook engineers frantically fix or roll back the problem to limit the amount of time Facebook is unusable.

2.) 1 in 5 news feed items shows "NaN people liked this", but Facebook is otherwise usable. The flood of errors is reported to the error monitoring system and Facebook engineers frantically fix or roll back the problem to limit the amount of time the weird "NaN people" message is shown.

Scenario #1 is a really bad outage, and scenario #2 is a temporary curiosity/annoyance that most people don't notice, and hopefully it's clear that scenario #2 is a better situation for everyone. But it really depends on context; if the bug is "a bank's website shows incorrect account balances", then crashing the page is probably a better user experience.

I'm certainly not saying JS got it right; JS doesn't do the part from #2 where it alerts you if there's a non-fatal error in production. But I think the basic idea of error resiliency has plenty of merit.


The problem with "avoid crashing at all costs" is that the alternative is usually "produce invalid output". The latter doesn't sound so bad in theory - but in practice, it means that the resulting bad data can go quite a long way, further accumulating errors as it flows through the pipeline. Worse yet if there are any observable actions taken on the basis of bad data (like, say deleting a file, or deciding to show some piece of private data).


Python 2 has some strange behaviour of its own with regards to comparing objects of different types, for example:

    >>> None < 0
    True
    >>> '0' > 0
    True
Granted, this follows a much simpler rule than Javascript's comparison operators, and both throw TypeError in Python 3.


If you're wondering why JavaScript does this, it's because early versions of the language did not have exceptions. So every operation defined at the time has to return some value for every possible input, even when those inputs don't make any sense.

It's likely that the only way we could get rid of this behavior would be through some `use strict`-like opt-in semantics.

TypeScript helps a lot too: https://www.typescriptlang.org/play/#src=0%20%3E%3D%20null%3...


Dynamic languages with implicit type conversions are designed to make the easy cases easy (think of JS operating on webpage input, where everything is initially a string; would it have the influence and popularity it does today, if lots of explicit conversions had to be written?), but as a side-effect, the hard cases can become perplexing.

I have no doubt those steps specified in the standard had plenty of thought put into them. It has to handle all the types, as well as those special cases of infinities, NaNs, and +/-0, such that the common cases make sense. However, I think a flowchart or other graphical means of illustrating those algorithms would be far easier to understand.


you mean in this blog, or in the spec? (for the spec having the actual step spelled out is pretty much all you need when you're a spec implementation engineer. The flow chart is basically just eye candy for readers)


The rule `if null < 0 is false, then null >= 0 is true` seems to rely upon the law of the excluded middle, which is violated by the comparison algorithms. I think this summarizes the issue.


More precisely, it relies on the assumption that

(x > y) || (x == y) || (x < y)

is true for all values x, y.

But the semantics of the comparison operators lead to null > 0 || null == 0 || null < 0 being false and the whole house of cards crumbles down.


This is what comes from allowing conditionals on undefined values. The result of the relational operators isn't allowed to be "undefined". Nor is it an error to apply "if" to "undefined".

A similar problem comes up at the CPU level with IEEE 754 floating point arithmetic. There's a set of reasonable rules obeyed by FPUs about how results can yield a NaN. NaN values propagate through the math operations; if any operand of +, -, *, / is NaN, the result is NaN. That was well thought out.

But it breaks down at comparisons. Comparisons always return True or False. All comparisons with NaN return False. There's no such thing as "not a Boolean". Logically, there should be "not a Boolean" in compare condition bits, and attempts to use it to control a branch should cause an exception.

For historical reasons, FPUs were designed as add-ons to the main CPU. They were once separate "coprocessor" chips, back when one chip couldn't contain enough transistors to do both jobs. Early microprocessor FPUs had an arms-length relationship with the main CPU, and the x86 instruction set still reflects this. So the FPU comparison results and the branch logic aren't tightly integrated.

(There's also the fact that few languages can handle a floating-point exception well. You usually can't just put a try/catch around your number crunching and get an exception if the computation starts crunching meaninglessly on NaNs.)


In a game of code golf, or something where you purposely make your code as hard to read as possible, I can imagine doing something like `if (!(x>0) && !(x==0) && (x>=0))` instead of doing `if(x===null)`.


Not as hard, it's as short as possible, i.e. a keystroke is stroke.

The hard to read is a pleasant side effect.


Did he seriously claim at the end that that made sense "mathematically"?


When the language permitted a nonsense comparison like this, the spec needs to be complex, and within the logic of the spec, yes it makes sense.

The original error was allowing a nonsense comparison. Which is greater, 10 or fish? If you permit asking this question, you'll occasionally get weird results.


I propose we call Javascript a "quantum mechanical programming language" because the value of a variable can never be determined if it is compared to another by observing it directly.

I can just hear it now: "It helps to think of variable conversions in comparisons as a 'cloud' of value probabilities instead of a discrete value"


Yes. For X and Y being of the same type, defining the < operator should be enough to derive all other operators:

Define X < Y, and then derive the rest:

X > Y : X < Y

X == Y: !(X < Y) && !(Y > X)

X != Y: !(X == Y)

X >= Y: !(X < Y)

X <= Y: !(Y < X)

That's why it's the only operator that needs to be defined for std::map/set (which are rb-trees) in C++

Now, if X and Y aren't the same type, the only thing you can expect to get out of the system is a cronenberg.


> X == Y: !(X < Y) && !(Y > X)

Only for totally ordered sets [1]. Floating-point numbers, for example, are not totally ordered, because !(NaN == NaN).

[1] https://en.wikipedia.org/wiki/Total_order


Both the > and >= make sense. Having a >= b <=> !(b > a) ensures consistency (though it requires your set is at least weakly ordered, partial ordering is not enough, and NaNs break everything as usual...) This time IMO it's really the == that's broken. Given that it's really lenient with implicit conversions anyway, it should also apply ToNumeric to null, returning 0==0 which is, of course, true. If you want strict equality you'd use === as usual.


It makes sense when you're dealing with real numbers, which I think is what he's getting at. It only falls apart when you try using the operator with other types.


Right, which is where I think the actual return value ought to be "undefined" or something. Or an error. But I'm clearly in the minority on my thoughts of the js type system.

But then again, you can avoid having to make intertype comparisons in the first place by following reasonable guidelines. As funny as this case is, I can't imagine how I'd run into it in practice.


bottom line is: if your language allows you to compare apples to oranges, then you are comparing apple to oranges.


I thought so when starting to read this article and seeing > and == cases being false. I guess, with some experience one probably has implemented such logical shortcuts as well themselves. And now I am pretty sure this is a question of backwards compatibility and not anymore easily reversible


I think a lot of you are drawing overly broad conclusions from this, according to your predispositions.

A good language feature should be intuitive, given a basic understanding of the feature, and this falls within that.

A JavaScript programmer considering what the relative order of null and 0 is (and therefore how an inequality operator between them would behave) would intuitively conclude, "I don't know, 0 and null don't have a natural relative order. I probably shouldn't be doing that or else I need to go to the spec."

Anyway, you can hardly judge a language feature as bad because it alllows you to do things that don't make sense (in an edge cases, no less)... then what language features in any language be good?


> then what language features in any language be good?

Those that obey general principles without exception. For example:

(0) “Abstraction clients must not rely on implementation details”: parametric polymorphism, abstract data types.

(1) “Case analyses must be exhaustive”: algebraic data types and pattern matching.

(2) “Make as few assumptions possible about the intended meaning of programs”: principal types.


I wish in JavaScript that inequality operators would always return false if either side of the operator needed coercion, at least then this sort of scenario would be predictable (and a little more JavaScripty than throwing an error).

As it stands, it's bad practice to rely on this sort of edge case in your code. If you know that one of the variables can be null, always handle that case before doing the comparison.



See the biggest problem with JS dynamic types isn't "type uncertainty" but coercion. Fail instead of limping along ffs.


I think the right response for such an operation would be to throw a runtime error as opposed to coercing it into an integer. That's what a lot of other languages like ruby do. While I understand JS' philosophy of dynamic typing and coercion rather than runtime errors. I think this is taking it too far.


Here is a nice investigation on how such curious javascript behaviors can have an impact on a real code base.

http://stedolan.net/incomparable/


The least favorite things of all developers... Nulls and JavaScript.


I had to analyse some javascript malware the other day. It was quite impressive, and reminded me about some of the hateful parts of javascript.


Life is too short to understand why JavaScript acts the way it does.


It ain't no preorder


Dynamic typing is a nightmare.


It's not so much dynamic typing, but a lack of typing. Dynamic typing means that type checks are made at runtime.

e.g. in Ruby:

    irb> nil >= 0
    NoMethodError: undefined method `>=' for nil:NilClass
and Python 3:

    >>> None >= 0
    TypeError: '>=' not supported between instances of 'NoneType' and 'int'
Obviously JS (and Python 2!) get this wrong, but to be fair JS was designed in the 90s.


Someone else pointed out that Java has (or had?) a similar issue with comparing boxed Ints -- equality checks for object level equality while inequality compares the values.

Without explicit casting, the equality operator casts to a more general type (or rather, doesn't cast at all) than the inequality operator (which casts to numerics).

This happens anywhere you have weird coercion rules.


> Java has (or had?) a similar issue with comparing boxed Ints -- equality checks for object level equality while inequality compares the values.

In Java 5 and up, 'new Integer(1) < new Integer(2)' will return true, because it automatically unboxes both Integers and compares them as primitives (before Java 5 it was a compile error).

It will not unbox the Integers in 'new Integer(1) == new Integer(1)', though, because given objects, '==' is always reference equality, so it will return false because they're different instances ('!=' would still return true, though). To compare objects by value, you use equals; 'new Integer(1).equals(new Integer(1))' is true.


I agree with everything you said, except for the part where you said JavaScript was "designed".


Haskell was designed in the 90s. So don't blame the decade.


Plenty of languages designed before the 90s got this right. It's not like typing is a recent development.


While I dislike dynamic typing in general, in this case the nightmare comes from aggressive and inconsistent coercions. You could have the same mayhem in a statically-typed language with poorly-considered implicit typecasts.


No. In SNOBOL4 (one of the first dynamic typing languages) -- ident(x) yields true if x is "null", eq(x) is true if x is 0, gt(x) is ">" and ge(x) is ">=". The program "code" is a repl.

Notice -- free format dynamic typing, and, it gets it "right". This language/run-time was originally from the late '60s, last official update in 1975 (with some updates in the intervening decades).

Yes, according to the Javascript spec, the JS behaviour is correct. However, something as old as SNOBOL4 should have taught us that keeping type converting numerical operators dis-similar from object comparision would be good: "==" and "===" vs ident() and eq() and then relating ge() le() etc to the numeric class of operations consistently.

  : fred@dejah wittgenstein $; code
  The Macro Implementation of SNOBOL4 in C (CSNOBOL4BX)   Version 2.0
      by Philip L. Budne, January 1, 2015
  SNOBOL4 (Version 3.11, May 19, 1975)
  BLOCKS (Version 1.10, April 1, 1973)
    Bell Telephone Laboratories, Incorporated
  EXTENSIONS (Version 0.25, June 16, 2015)
    Fred Weigel

  No errors detected in source program

  CODE (TUE AUG  4 10:28:58 EDT 2015)
  RUNNING ON CSNOBOL4 MAINBOL WITH SPITBOL, BLOCKS, EXTENSIONS
  ENTER SNOBOL4 STATEMENTS (TRY ? FOR HELP)
  5,541,616 BYTES FREE
  CODE: ident()
  SUCCESS
  CODE: ident(0)
  FAILURE
  CODE: eq(0)
  SUCCESS
  CODE: eq()
  SUCCESS
  CODE: gt()
  FAILURE
  CODE: ge()
  SUCCESS
  CODE:quit
  Normal termination at level 1
  code.lss:660: Last statement executed was 938
  SNOBOL4 statistics summary-
          5.384 ms. Compilation time
         90.585 ms. Execution time
          20577 Statements executed, 121 failed
          20004 Arithmetic operations performed
            122 Pattern matches performed
              2 Regenerations of dynamic storage
         29.481 ms. Execution time in GC
              0 Reads performed
             10 Writes performed
       4402.237 ns. Average per statement executed
        227.157 Thousand statements per second
  : fred@dejah wittgenstein $; 
and, keeping in the spirit:

  CODE: ge('a')
  EXECUTION ERROR #1, Illegal data type
  FAILURE
Allow the explicit capture of "type" errors. We knew this in 1975. Why was this forgotten?


Lack of formal training in Computer Science. Most programmers are self-taught and know nothing about the history of the field or prior work.


Done right (like in python), it's not so bad.


I really wish the industry settled on terms for this stuff but alas...

The difference is python is strict about its types and doesn't do automatic coercion. So at least you get type errors at runtime and not magic implicit behaviour.

That said, even Perl did this better (less magic and surprises)... JavaScript is like a language intentionally designed to surprise the developer. Half the time I feel like it's a practical joke that someone accidentally took seriously.


> I really wish the industry settled on terms for this stuff

I've pretty consistently heard "strong typing" to mean that the language avoids coercing between types, and "static typing" to mean that types are known at compile time. So JS and Python are both dynamic typed, but Python is (more) strongly typed and JS is (more) weakly typed.

https://stackoverflow.com/questions/2690544/what-is-the-diff...


Yes, I've always understood this to be the correct terminology, but unfortunately in informal discussion among non-academically inclined software people, people use the terms wrong more often than they use them right. You get lots of people saying C is strongly typed (it certainly is not) and something like Scheme is weakly typed (it certainly is not).


If you're really want a fun time, try having a conversation about "duck typing" some time... it was around the time that term became a thing that I realized no one seems to agree on what all these terms mean.


Using "strict" vs. "non-strict" and "static" vs. "dynamic" will increase a lot the odds of people understanding.


the problem is there is no "right" until you define what right means, which is where the spec comes in. These results are "right", if unexpected, and the true issue here is of course not so much the mathematical results between the three tests, but the fact that JS gives you the freedom to compare anything to anything, irrespective of whether that comparison makes sense at all. Python allows this in a few places, too, with equally bizarre results (see another comment that highlights a few of them).

While tempting to do so, thinking of >= as implying "> or ==" is simply not true in any language that performs type coercion to get around type incompatibility issues.


Yes I can contrive examples in any language that are nightmarish. Fortunately we manage to muddle through somehow, at least those of us who have work to do in lieu of walking through trivia to prove how smart we are to other hackers.


Okay.

Find an equivalently surprising example in... Let's use the other poster's choice, python.

I'll wait.

Edit: didn't have to wait long, way to go Python! Maybe I'll just claim it probably has fewer of these types of bizarre idiosyncrasies? I hope it has fewer...

I'm always baffled by JavaScript apologists... Look, your baby is ugly. We know it's ugly. You know it's ugly. But if we point out the problems, hey, maybe the ECMA will update the language to fix some of the issues.

Meanwhile, being aware of them is often important in order to build correct, secure code.

And they're just entertaining.

So chill. Let's all laugh at how ugly JS is on a Saturday morning and enjoy ourselves a bit. Because when it comes to the JS ecosystem, if we can't laugh, we'd probably be crying...


I find this behavior surprising:

  >>> d = {0: "int"}
  >>> d[False] = "bool"
  >>> d
  {0: 'bool'}
  >>> nan = float('nan')
  >>> d[nan] = 0
  >>> d[nan] = 1
  >>> d
  {0: 'bool', nan: 0, nan: 1}
Yes, yes, it's because issubclass(bool, int) == True, and nan != nan, but weird.

And if we include Python 2 you get the truly ridiculous:

  >>> dict() < set()
  True
  >>> dict < set()
  False
  >>> 0 < dict
  True
  >>> 0 < 1j
  TypeError: no ordering relation is defined for complex numbers
To compare e.g. a dictionary and set we compare the _names_ of the types! Except complex numbers, because they don't have an ordering. I actually have come across this as a bug in production.

Personally I'd just make any comparison involving different types an error. That probably interacts poorly with subtyping, but it's a price I'm willing to pay.


The `nan` thing is actually because `float('nan')` can (and does) return a new object (well, with a different ID) each time. So it's like going `d[object()] = 'a'; d[object()] = 'b'` and not retaining the objects for lookup.

> Personally I'd just make any comparison involving different types an error

As you point out, this is what Python 3 did for `None`. But it's been idiomatic to have 0 = False when languages didn't have `False`; so `bool(0) == False` makes sense, but also `sum(boolean for boolean in sequence)` works. So you could argue that for `True` and `False`, meaningful coercion is possible, and in reverse, if zero is false-y and everything else is truth-y (like in C), then that also makes sense. Imagine if you had to cast every number to a boolean explicitly! So I guess having `d[0] == d[False]` is the lesser of evils.


My favourite of these is Haskell

> "" = [] True

Which makes perfect sense if you know any Haskell, but looks like a JavaScript wat if you don't.


I gave a talk on Python idiosyncrasies a while back that you might enjoy:

https://speakerdeck.com/alangpierce/python-puzzlers

Several of the examples from that talk have equivalents in JavaScript where JavaScript does better. I think pretty much any real-world language has lots of surprises like this that you need to get used to, although I certainly admit JavaScript (particularly JS type coercion) tends to have especially surprising behavior.


I'd say JSs biggest problem is that the surprising behaviours aren't mostly sitting in dark corners waiting to bite you. They're standing there in the open, where everyone is likely to encounter them.


> Find an equivalently surprising example in... Let's use the other poster's choice, python.

> I'll wait.

I don't write Python, but even so it took about 30 seconds, using the same area of the linked article as a starting point:

    >>> 0 > None
    True
At this point, I fully expect some attempt at a post-hoc rationalization for why Python's choice here is unimpeachable.


python 2 made a rational, well-considered choice, though I do like the python 3 replacement better. Despite its self-consistency, python 2 ended up causing hard-to-find errors (mostly when people accidentally compared "0" with 0 though; it mostly wasn't a problem with None).

In python 2, cross-type comparisons are strictly ordered. So:

    >>> None == 0
    False
    >>> None < 0
    True
    >>> None <= 0
    True
    >>> None > 0
    False
Completely consistent, no special cases, and if you sort() a list containing multiple types, it'll always come out in an unsurprising, consistent order.


Is this python 2 or python 3, afaik that was fixed in python 3.


This error has been fixed in Python 3.


Nope, that's pretty ridiculous!


Forget JS! WebAssembly is coming and you can use nice languages with it, for example C#. and if you need compatibility with older browsers you can still compile WebAssembly to JS and send it to old browsers.


That's nice. But also a new attack vector.


*or




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: