Floating Point in the Browser, Part 1: Impossible Expectations
randomascii.wordpress.com
randomascii.wordpress.com
So if you're trying to publish data from a GPS chip that reports fix failure by setting the latitude and longitude to a NaN (possibly with a failure code in the exception value) you can handle that in JS but only through some intermediate representation in JSON.
I spent a lot of time writing the number handling code in PBNJSON for WebOS back in the day (IIRC there were 0 C/C++ parsers that offered correct number parsing at the time). It also makes sure to properly report overflow/underflow/failure to convert whenever you encounter situations that aren't possible (e.g. reading 32-bit number of a 64-bit int JSON, reading 64-bit integer for doubles that are out of range, etc). I'm sure I missed some corner cases though as it's insanely difficult to get every single corner case (especially with floating point).
Well, RFC 8259 says "This specification allows implementations to set limits on the range and precision of numbers accepted." and goes on to explain interoperability with IEEE doubles.
As soon as you have something in your pipeline that converts to floats, it is poisoned. Lots of implementations you may use do this conversation though (e.g. basically anything JavaScript, jq) so it is pretty easy to get float poisoning.
The actual solution to representing big bums in practice is to wrap them in double quotes. It only costs two bytes (and if you care about those bytes then you should probably not be using json.
And having to have special code to handle getting it as a string instead of a number.
Some language implementation bug trackers have a warning not to submit bugs for it front and centre because they get it so often.
[e] https://github.com/ulfjack/ryu
https://github.com/llvm/llvm-project/tree/master/flang/lib/D...
https://randomascii.wordpress.com/category/floating-point/
In general the whole blog is super interesting and many times submitted to HN:
https://news.ycombinator.com/from?site=randomascii.wordpress...
These days, if you need huge integers, you can use BigInt, but if you want to use it in an expression that also contains floating point values, then you need to explicitly convert it. Explicit is better than implicit.
The expectation is that sorting an array of integers in place:
[1,3,2,4,5,7,6,8,9,10].sort()
produces an array with the numbers in ascending order (since that's what Ruby/Python do). Not so with JS (try it out).A agree there is a perfectly logical reason for every quirky behavior, but I've been surprised (in a bad way) so many times by JS's counter-intuitive behavior that I now have a mental collection of gotchas where JS deviates from expectation.
JavaScript actually has a formal specification. Ruby and Python have no formal specification. I can tell you (with reference to the specification) exactly what a JavaScript program does. JavaScript may seem wacky on the surface, but actually it's all worked out. Nobody can tell you what a Ruby or Python program does.
And you think JavaScript is the footgun?
"Theory is great in theory, but in practice, practice is better"
JS has many quirks, but I like that the numeric representation is extremely simple and explicit (even compared to Python).
[1,3,2,4,5,7,8,9,10].sort((a,b) => a - b) gives you the behavior one would expect.
I suppose you already know that, though. Agreed there are lots of quirks.
If I understand correctly, Python had a more considered initial design, and had a clean-break overhaul in Python3, allowing it to evolve without accumulating as much cruft and inconsistency. (Python is famous for its philosophy of there should be one—and preferably only one—obvious way to do it.) People use Python by choice: unlike JavaScript where browsers support no other scripting languages, Python won out over its rivals in fair contest.
All things considered, JavaScript isn't that bad. Used correctly it can be elegant and powerful. It has some nasty dark corners, but it generally provides all you need to avoid them, if you know your way around. With modern JIT engines it can even be pretty fast. Its curious use of doubles is just the kind of thing modern JIT engines are able to be smart about, compiling down to use integer operations where possible.
I disagree. Double was the best choice they could have made for a dynamically typed language.
JavaScript, the language, doesn’t (AFAIK; I don’t keep track of this rapidly moving target) have integers (but does have integer arrays, nowadays. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Type...)
If the type were integer,
let x = 2
let y = x / 8
would have y = 0, but its value will be 0.25.I'd bat both eyelids, that's the kind of surprising design decision that would turn me away from a language. To my knowledge, no major programming language does this, and there's wisdom in the principle of least surprise.
edit: I am of course mistaken, Python does exactly this. Corrected by chrisseaton again!
Lol prepare to bat your eyelids.
% python
Python 3.8.5 (default, Jul 21 2020, 10:48:26)
[Clang 11.0.3 (clang-1103.0.32.62)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 1 / 2
0.5If you're up for a game of No True Scotsman, can you name me a statically typed language that does the same?
https://docs.microsoft.com/en-us/dotnet/visual-basic/languag...
If you don't believe me, perhaps you'll believe output from the machine.
https://tio.run/##K0vSTc4vSv3/3zc/pTQnVcEjNScnn4szuDRJwTcxM0...
* edited for guideline compliance
> Did you read it?
The HN guidelines ask that you don't do that.
I don't think it settles anything, but a trivial function that divides two integers and returns the result compiles to this:
IL_0000: ldarg.1
IL_0001: conv.r8
IL_0002: ldarg.2
IL_0003: conv.r8
IL_0004: div
IL_0005: ret
conv.r8 is an explicit conversion to double precision float in the target language, MSIL.[0] https://nim-lang.org/docs/manual.html#types-preminusdefined-...
```
GHCi, version 8.6.5: http://www.haskell.org/ghc/ :? for help
Prelude> 1 / 2
0.5
```
This is because 1 and 2 are polymorphic and get instantiated to floats here. Witchcraft.
Note: there are several potential instances:
instance RealFloat a => Fractional (Complex a)
-- Defined in ‘Data.Complex’
instance HasResolution a => Fractional (Data.Fixed.Fixed a)
-- Defined in ‘Data.Fixed’
instance Integral a => Fractional (Ratio a)
-- Defined in ‘GHC.Real’
...plus two others GHCI> :i (/)
class Num a => Fractional a where
(/) :: a -> a -> a
...
-- Defined in ‘GHC.Real’
infixl 7 /
The key is that integer types aren't Fractional, so the Haskell 2010 defaulting rules[1] cause the literals to be interpreted as Double. With an appropriate default declaration GHC could have picked Ratio Integer, Complex Double, etc. instead, but without one Integer and Double are the only options and Integer wouldn't satisfy the constraints.[1] https://www.haskell.org/onlinereport/haskell2010/haskellch4....
https://docs.julialang.org/en/v1/manual/integers-and-floatin...
JSON's arbitrary precision decimals is the silliest footgun. When every implementation implicitly defines its own valid format by way of what native types it maps it makes for a really awkward way to pass messages between two black-box systems.
It doesn't help there are IEEE standards for fp calculations which are also very misunderstood. The point of these standards is to provide reproducibility and repeatability so that different types of software running on different machines or with different compilers provide same results.
The goal of floating point never was to provide precise representation or calculation and in fact it is not possible to provide in general with floating point. The goal was to make it easy and relatively fast to perform real world calculations.
You understand your calculator gives approximate results except for very special cases, so why would you bicker over some digits that are very far to the right of your result.
It is actually much worse with calculators because they tend to employ very different calculation engines and very different precision. You can actually identify a particular model or model family of calculator by running couple calculations and finding what kind of artifacts are present in the results, exactly.
Yet nobody will tell you calculators are useless as I frequently hear about floating point.
I work with Java and there seems to be this movement recently where developers use arbitrary precision arithmetic to do any sort of calculation on anything without actual idea of when this is needed. Supposedly flating point is broken and cannot be trusted. Sigh...
Of course, the right solution is to use the right tool for the job: algorithms that either use a better structure than a 64-bit float, or are smartly designed enough to avoid their inadequacies. But how many developers have both enough insight and enough development time to do that? Everything is a compromise; fixed-point decimal representation is one that works for many applications.
It's like giving someone a $10k check and seeing them get $9,999.99, and then getting irritated when people try to investigate what happened because it's only a penny to you. It's kind of missing the point that the error logically shouldn't be there in this situation.