elm-format
elm-format copied to clipboard
Float constant is changed to incorrect value
When I try to define a constant as
maxFloatAsFloat : Float
maxFloatAsFloat =
18446744073709549568.0
elm-format
changes that to
maxFloatAsFloat : Float
maxFloatAsFloat =
18446744073709550000.0
This is incorrect. Value 18446744073709549568.0
can be represented exactly as a Float
unlike 18446744073709550000.0
.
p.s. This is largest possible Float
value less than 2^64
, i.e. 2^64 - 2048
.
An Elm/JS Float is a double precision floating point number https://en.wikipedia.org/wiki/Double-precision_floating-point_format Even though it uses 64 bits it doesn't have 64 bits worth of precision, trading off some of it for extra range (exponent). It has 52 bits of mantissa, which is 53 bits worth of precision (1 bit is implicit).
You can test your number in the JS console of your browser:
> 18446744073709549568.0
18446744073709550000
To show you what I mean about the limit of precision, you can test the following (2 ** 53) + 1 === 2 ** 53
which is true
since the floating point format doesn't have enough precision to store that extra 1
.
You can still represent numbers bigger than 2**53
, just not all of them since you're hitting precision limits.
Hope this helps :smile:
You don't understand the issue. I understand perfectly how floating point precision works.
Your example is incorrect because JS console doesn't show exact value but rounds it.
Here is correct example (using malaire/elm-uint64
):
$ elm repl
> import UInt64
> UInt64.floor 18446744073709550000.0 |> UInt64.toString
"18446744073709549568" : String
> UInt64.floor 18446744073709549568.0 |> UInt64.toString
"18446744073709549568" : String
So while you are correct that not all values can be represented as Float
, you are wrong about which value can be represented as Float
. Number 18446744073709550000.0
can't be represented as Float
but is rounded to 18446744073709549568.0
, while 18446744073709549568.0
is not rounded as it can be represented exactly as Float
.
It looks like various languages disagree on how to do double to string conversions:
Rust:
fn main() {
println!("{:?}", 18446744073709549568.0_f64);
println!("{:?}", 18446744073709550000.0_f64);
}
➜ rustc test.rs && ./test
18446744073709550000.0
18446744073709550000.0
C:
#include <stdio.h>
int main() {
printf("%lf\n", 18446744073709549568.0);
printf("%lf\n", 18446744073709550000.0);
return 0;
}
➜ gcc test.c && ./a.out
18446744073709549568.000000
18446744073709549568.000000
Haskell (how elm-format
is implemented):
import Text.Printf
main = do
printf "%f\n" (18446744073709549568.0 :: Double)
printf "%f\n" (18446744073709550000.0 :: Double)
➜ runhaskell test.hs
18446744073709550000.0
18446744073709550000.0
It might be wrong but then your issue isn't really with elm-format
as much as it is with these various languages and their standard libraries.
I would be interested to know why they're giving different results though :sweat_smile:
It looks like various languages disagree on how to do double to string conversions:
It might be wrong but then your issue isn't really with elm-format as much as it is with these various languages and their standard libraries.
Since there is disagreement on how Float
values like this should be represented, elm-format
should leave Float
literals as-is and not change them. So this is clearly elm-format
issue.
Looks like the rust
conversion to uint64 does the same as you: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b1890ec671149101edbed17be38f3788
I agree that a formatter could treat literals as blackboxes (String) and print them as is, that would also solve other problems like this one: https://github.com/avh4/elm-format/issues/635
A goal of elm-format is to format code canonically, such that it makes the actual behavior of the code more clear.
This example program: https://ellie-app.com/b6hgdKMgnFxa1
module Main exposing (main)
import Html
main =
Html.text (Debug.toString 18446744073709549568.0)
displays the output "18446744073709550000"
So I think formatting it as
main =
Html.text (Debug.toString 18446744073709550000.0)
is correct.
malaire's example with UInt64.floor
also has the same behavior after being formatted, so I think the change is also correct in that case.
Are there other edge cases I'm missing? Can someone provide an example program where formatting it changes the behavior of the program?
A goal of elm-format is to format code canonically, such that it makes the actual behavior of the code more clear.
Exactly my point. Value 18446744073709549568.0
stays exactly same when put into Float
while 18446744073709550000.0
does not stay same - it's converted to 18446744073709549568.0
.
So 18446744073709549568.0
should be kept as-is and not changed to rounded inexact value.
Your example with Debug.toString
is invalid because Debug.toString
rounds the value and doesn't show it exactly.
Can someone provide an example program where formatting it changes the behavior of the program?
It doesn't change behavior - both 18446744073709549568.0
and 18446744073709550000.0
become same value as Float
- i.e. 18446744073709549568.0
.
Can you show an example of some running code that demonstrates how the value is 18446744073709549568.0 and not 18446744073709550000.0 ? I haven't been able to come up with one yet.
Can you show an example of some running code that demonstrates how the value is
18446744073709549568.0
and not18446744073709550000.0
?
I already showed code which shows that 18446744073709550000.0
is "rounded" to 18446744073709549568.0
:
$ elm repl
> import UInt64
> UInt64.floor 18446744073709550000.0 |> UInt64.toString
"18446744073709549568" : String
Here UInt64.floor
converts the value it receives exactly to UInt64
, which is then converted exactly to String
.
UInt64.floor
doesn't receive 18446744073709550000.0
as argument because that value can't be represented as Float
, but it receives nearest value that can be represented as Float
, i.e. 18446744073709549568.0
.
When looking these values at base-2 it's clear that 18446744073709549568.0
is the actual "rounded" Float
value and
18446744073709550000.0
is too exact to be represented as Float
, which only has 53-bit mantissa:
18446744073709549568 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000 0000 18446744073709550000 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1001 1011 0000
They seem to have the same bits in rust. Maybe I am missing something?
Hmm, yeah, malaire, I guess that makes sense, that 18446744073709550000 can't be represented.
Do you happen to know why 0x 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000 0000
gets to-stringed as 18446744073709550000 both in browsers (and in Haskell)? They're rounding to 16 decimal digits for some reason?
@harrysarson Those bit values I showed above were 64-bit integer values, not Float
values.
As I've said 18446744073709550000.0
can't be represented as Float
but is always converted to 18446744073709549568.0
so your Rust example shows 18446744073709549568.0
as Float
in both cases.
@avh4 Actually 17 digits (17th here happens to be zero) because any Float
, when rounded to 17 digits, can be converted back to original value. It just seems to be a common custom to round the values as much as they can be rounded without changing behavior.