razuf
Hi everybody,
I found a bug or structural problem in Decimal what could have some big impact:
wrong calculation (maybe with money!!!) or even infinite loop ;-(
My suggestion for a fix is on pull request:
https://github.com/ericmj/decimal/pull/157#issue-436023781
Does anybody have another idea - there are some possible ways to handle validation for structs - but what would you suggest is the best way?
Any suggestions are welcome! ![]()
Here are one solution example for division:
defmodule Decimal do
@type coefficient :: non_neg_integer | :NaN | :inf
@type exponent :: integer
@type sign :: 1 | -1
@type t :: %__MODULE__{
sign: sign,
coef: coefficient,
exp: exponent
}
defstruct sign: 1, coef: 0, exp: 0
...
def div(%Decimal{coef: coef1}, %Decimal{})
when coef1 < 0,
do: error(:invalid_operation, "dividend (#{coef1}) must be > 0", %Decimal{coef: :NaN})
def div(%Decimal{}, %Decimal{coef: coef2})
when coef2 < 0,
do: error(:invalid_operation, "divisor (#{coef2}) must be > 0", %Decimal{coef: :NaN})
the orig pull request text - if don’t want to follow the link:
Hi,
I found a bug or structural problem in Decimal what could have some big impact under special circumstances. Here an example from the Money lib where Decimal is used as main backbone:
iex(15)> m = Money.new(:USD, Decimal.new(%Decimal{sign: -1, coef: -12000000000}))
#Money<:USD, --12000000000>
iex(16)> Money.mult(m, 3)
{:ok, #Money<:USD, --36000000000>}
iex(17)> Money.mult!(m, 3 )|> Money.to_string
{:ok, "$--*,000,000,000.00"}
Of course it’s a special edge case … normally the best practice is:
iex(18)> m = Money.new(:USD, "-12000000000")
#Money<:USD, -12000000000>
but you never know … maybe someone using it …it’s possible! it’s valid Code ! NO compiler warning! NO runtime error!
And the worst case, if you are using any division with this kind of Decimal number like this:
iex(19)> m = Money.new(:USD, Decimal.new(%Decimal{sign: -1, coef: -12000000000}))
#Money<:USD, --12000000000>
iex(20)> Money.mult!(m, 3) |> Money.to_decimal |> Decimal.to_float
→ you end up in an infinite loop!!!
The reason for this is clear: the %Decimal{} struct - with no validation for input → so you can input a negative coefficient.
The type spec and docu are perfect - but do not prevent any misuse:
Type spec : @type coefficient :: non_neg_integer | :NaN | :inf
Documentation: The coefficient of the power of 10. Non-negative because the sign is stored separately in sign.
So my suggestion for a solution you can find in this pull request: mostly checks for this special edge case.
And this time I included the corresponding tests. ![]()
Kind regards and I hope it helps anybody to use Decimal in a safer way!
Ralph
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
bglusman
I think the intended use here and for most structs is to not construct them yourself with raw internals but to use the library as the authority on validating/constructing… in this case,
Decimal.newhandles this by settingcoefto an absolute value decimal/lib/decimal.ex at main · ericmj/decimal · GitHubI’ll let @ericmj or other maintainers decide if they’re also interested in this additional safeguard, and I’ve been burned by constructing a struct incorrectly myself like this before, but, worth knowing as a general rule in elixir.
NobbZ
The coefficient type is clearly typed as non negative integer. Even though I’d not necessarily expect the library to crash on malformed data, I’d at least not expect it to behave correctly.
So any PR “fixing” this should do so by crashing because of function clause errors or badarg at least.
razuf
yes - the fix does exact that - anyway I was interested on your opinions and ideas. Thanks.
razuf
Maybe the Norm lib:
https://github.com/keathley/norm
al2o3cr
Nitpick: that specific code
will be detected by Dialyzer:
because Dialyzer can be 100% certain that
-12_000_000_000is not anon_neg_integer().However, using a variable gets it to pass:
BUT I’m not sure how far library code should go to defend its invariants against intentional manipulation.
For instance, it’s possible to produce a
MapSetwith strange behavior at runtime:but that code will always cause a Dialyzer error because of the
@opaquesetting on MapSet.There are some gotchas with
@opaque, notably in pattern matching and module attributes which may explain why it isn’t used inDecimal.benwilson512
Yes exactly. Elixir is dynamically typed. If you abuse that to create malformed types they won’t work properly. This is true of literally every struct.
razuf
Thanks all for commenting.
@benwilson512 : Of course! I know. But does that mean we should not fix error edge cases? → I mean NO. And I think you agree with me and if we can we should make it easier to use and more safe for everybody.
As NobbZ wrote : So any PR “fixing” this should do so by crashing because of function clause errors or bad arg at least.
And thats exactly what it does. So back to the focus :
I think too the best bevavior for the existing Decimal lib and all the depending libs are: let it crash if wrong inputs:
all other wrong cases are handled by function clauses
So I think better then the current behavior with all the consequences of prolonging the error (see above):
michallepicki
I think in a dynamically typed language, it is possible for a library to detect many misuses and edge cases at runtime, but there is a runtime cost to doing that. Documenting proper usage and data types allows to lower that runtime cost (assuming the documentation and type specifications are being followed)
razuf
yes something like this…
I think as much as possible without breaking the target. And it’s possible in this case.
Thanks for your thoughts!
razuf
yes you are right! There is a cost for it. I will try to measure it …