14 ms·
JavaScript: The Curious Case of null >= 0
- recentdarkness 9y agoI 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
- infogulch 9y agoDynamic typing is a nightmare.
- gruez 9y agoDone right (like in python), it's not so bad.
- zzalpha 9y agoI 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.
- alangpierce 9y ago> 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-difference-between-a-strongly-typed-language-and-a-statically-typed https://stackoverflow.com/questions/2690544/what-is-the-diff...
- mikebenfield 9y agoYes, 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).
- zzalpha 9y agoIf 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.
- marcosdumay 9y agoUsing "strict" vs. "non-strict" and "static" vs. "dynamic" will increase a lot the odds of people understanding.
- TheRealPomax 9y agothe 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.
- Pxtl 9y agoWhile 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.
- outoftacos 9y agoYes 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.
- zzalpha 9y agoOkay. 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...
- alangpierce 9y agoI gave a talk on Python idiosyncrasies a while back that you might enjoy: https://speakerdeck.com/alangpierce/python-puzzlers 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.
- zzalpha 9y agoI'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.
- carussell 9y ago
- namelost 9y agoIt'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.
- SomeStupidPoint 9y agoSomeone 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.
- kbp 9y ago> 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.
- matt2000 9y agoI agree with everything you said, except for the part where you said JavaScript was "designed".
- wwwkeykey 9y agoLOL best comment :D
- deleted 9y ago
- ratboy666 9y agoNo. 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?
- laichzeit0 9y agoLack of formal training in Computer Science. Most programmers are self-taught and know nothing about the history of the field or prior work.
- userbinator 9y agoDynamic 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.
- TheRealPomax 9y agoyou 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)
- bga 9y agoDid he seriously claim at the end that that made sense "mathematically"?
- Sharlin 9y agoBoth 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.
- matthewaveryusa 9y agoYes. 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.
- adwn 9y ago> 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 https://en.wikipedia.org/wiki/Total_order
- ldjb 9y agoIt 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.
- bga 9y agoRight, 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.
- alangpierce 9y agoNot 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.
- deleted 9y ago[deleted]
- nine_k 9y agoAggressive coercion in an attempt to execute clearly faulty code is exactly the problem.
- microcolonel 9y agoBut 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.
- danschumann 9y agoIn 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)`.
- koolba 9y agoNot as hard, it's as short as possible, i.e. a keystroke is stroke. The hard to read is a pleasant side effect.
- acjohnson55 9y agoThe 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.
- lou1306 9y agoMore 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.
- spo81rty 9y agoThe least favorite things of all developers... Nulls and JavaScript.
- sAbakumoff 9y agoLife is too short to understand why JavaScript acts the way it does.
- Animats 9y agoThis 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.)
- roberttod 9y agoI 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.
- nebulous1 9y agoIt does. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equa...
- 567arlo 9y agoThis 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.
- alangpierce 9y ago> 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-handling-in-react-16.html 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.
- landryraccoon 9y ago> 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?
- nebulous1 9y ago*or
- jmull 9y agoI 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?
- catnaroek 9y ago> 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.
- gcb0 9y agobottom line is: if your language allows you to compare apples to oranges, then you are comparing apple to oranges.
- rictic 9y agoIf 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%3B%0D%0A0%20%3E%3D%20undefined%3B%0D%0Anull%20%3E%3D%200%3B%0D%0A'10'%20%3E%2010%3B%0D%0A'10'%20%2B%2010%20%3E%2010%3B%0D%0A https://www.typescriptlang.org/play/#src=0%20%3E%3D%20null%3...
- wwwkeykey 9y agoForget 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.
- dep_b 9y agoThat's nice. But also a new attack vector.
- wwwkeykey 9y agoJavaScript and women have something in common: both are producing unpredictable results.
- alkonaut 9y agoSee the biggest problem with JS dynamic types isn't "type uncertainty" but coercion. Fail instead of limping along ffs.
- quickthrower2 9y agoIt ain't no preorder
- singingfish 9y agoI had to analyse some javascript malware the other day. It was quite impressive, and reminded me about some of the hateful parts of javascript.
- tejas1mehta 9y agoI 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.
- dwenzek 9y agoHere is a nice investigation on how such curious javascript behaviors can have an impact on a real code base. http://stedolan.net/incomparable/ http://stedolan.net/incomparable/