-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document protocol implementations #220
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for creating this PR.
I really like the explanation that is provided in ActiveSupport in the Rails framework. I'm sure developers unfamiliar with the issue would appreciate an explanation like that. 👍
|
||
iex> encoder = fn | ||
...> %Decimal{} = decimal, _encoder -> | ||
...> decimal |> Decimal.to_float() |> :json.encode_float() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know we can't directly use Decimal.to_string/1
but the precision loss via float shouldn't be assumed acceptable. If we had a Decimal.finite?/1
or Decimal.number?/1
function this example could be more accurate:
if Decimal.number?(decimal), do: Decimal.to_string(decimal)
# Or:
true = Decimal.number?(decimal)
Decimal.to_string(decimal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but the precision loss via float shouldn't be assumed acceptable
Not sure I follow. I believe this is Eric's and mine point all along.
In any case, sure, we could add something like the following to the example encoder.
if Decimal.inf?(decimal) or Decimal.nan?(decimal) do
raise ArgumentError, "#{inspect(decimal)} cannot be encoded to JSON"
end
However, Decimal.to_string("1.00") #=> "1.00"
and that is not the result we want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I see what you mean by the 1.00
example. But let's say you want to encode Decimal.div(1, 3)
. You will lose quite a few decimal places when you convert to a 64-bit float first...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am misunderstanding something or we are talking past each other. Yes, we are losing precision, that's why we want to use strings not floats!
iex> Decimal.div(1, 3) |> Decimal.to_string() |> JSON.decode!() |> dbg
Decimal.div(1, 3) #=> Decimal.new("0.3333333333333333333333333333")
|> Decimal.to_string() #=> "0.3333333333333333333333333333"
|> JSON.decode!() #=> 0.3333333333333333
Same with Deno:
$ deno
> JSON.parse("0.3333333333333333333333333333")
0.3333333333333333
>
I don't see the point of encoding to a higher precision something that is expected to be a float. My understanding is decoders convert to float and lose that precision anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're basically talking about the same thing, but your intention is now clearer to me.
Since the default (protocol impl) behaviour is to encode a Decimal
as a string, isn't the point of overriding the protocol encoder in the example to show how to encode it as a JSON number instead? If the client you intend to send the JSON to, cannot handle JSON floats properly, it would of course not make any sense to do that. So we normally would do that when the API requires it or we want to store floats in a database JSON column.
If you return a string like "0.3333333333333333333333333333"
in the encoder function it will become a JSON float. No problem there unless you have to parse it with JS. But even in Elixir you will get an inaccurate float, unless you provide the :float
option and construct a Decimal using Decimal.new/1
.
I hope I could clear the confusion. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have examples on JSON implementations that can decode literal JSON floats lossless? My understanding is that most implementations, including Elixir which we are using in the example here, would decode it as lossy IEEE 754 float.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think implementations would decode as floats by default but at least in ruby you can opt-in:
$ irb -r json -r bigdecimal
irb> JSON.parse('0.3333333333333333333333333333')
=> 0.3333333333333333
irb> JSON.parse('0.3333333333333333333333333333', decimal_class: BigDecimal)
=> 0.3333333333333333333333333333e0
but yeah. maybe cd6aa59 is a mistake because I have a feeling it will cause confusion to most people. The purpose of this piece of documentation is not to educate people on subtleties of having or not having IEEE 754 but to show that we can customize encoding. @ericmj feel free to revert cd6aa59, I'm done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Json itself is actually quite unaware of floats. It just has arbitrary presision numbers being a text based format. It completely depends on the parser or encoder to deal with how to map between runtime level number values and their json text representation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what we have now is enough. By default we have a safe arbitrary precision encoding using strings, we also have examples of custom encoding to IEEE 754 floats in the docs, and this example can of course be tweaked to whatever precision encoding you want.
Thank you @wojtekmach for the protocol implementation and documentation improvements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @wojtekmach for adjusting the example.
I think the example doesn't need to give a complete education but it should make sense. And the only good reason to override the protocol encoder is the need to do something different than the default. I don't see how the adjusted example could be confusing. I'm sure it's helpful for developers to know that you can output lossless JSON floats if required.
To cover a more general case for overriding protocol encoders, an example could be included in the Elixir documentation for JSON.encode!/2
. There isn't one currently.
Do you have examples on JSON implementations that can decode literal JSON floats lossless?
Wojteck already gave an example with Ruby. In the issue #219 referenced by this PR, I mentioned a few other languages that can do lossless decoding and encoding. Afaics, JS is probably the only major language that isn't capable of doing this.
Thanks for the improvements. 👍
4500ed4
to
f964044
Compare
Also, improve error message on Decimal.to_float for inf/nan.
f964044
to
08b45df
Compare
Also, improve error message on Decimal.to_float for inf/nan.