ericmj / decimal

Arbitrary precision decimal arithmetic

Home Page:https://hexdocs.pm/decimal/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature request: macro for literals

marcandre opened this issue · comments

Thanks for this nice library 🙌

There doesn't seem to be a macro to define literal decimals. Using Decimal.new("...") has the following issues:

  1. does not work in pattern match
  2. makes it easy to create needlessly inefficient code
  3. verbose

Example for 1:

assert %{amount: Decimal.new("666.42"), ....} = result
# => Compile error: cannot invoke remote function Decimal.new/1 inside a match

Example for 2:

defp do_some_calc(value)
  Decimal.add(value, Decimal.new("666.42"))
end
# to avoid multiple calls to `Decimal.new`, one could write instead:
@add_me Decimal.new("666.42") # not a bad idea as it names the magic constant anyways
defp do_some_calc(value)
  Decimal.add(value, add_me)
end

A macro could call new at compile time and thus enable pattern match (and less importantly efficient inline constants like in do_some_calc above)

It's not very difficult to write something to do that, but a builtin function and/or sigil could be nice. Here's a basic sigil I wrote for our use, let me know if you'd like a PR:

defmacro sigil_d({:<<>>, _, [string]}, []) do
  string
  |> Decimal.new
  |> Macro.escape
end
  
assert %{amount: ~d(666.42"), ....} = result  # => works

I think this would be a nice addition, in fact we have such macro in our own test suite, but there's a few things worth mentioning.

A lowercase sigil, by convention, accepts an interpolation so that e.g. ~d(#{1 + 2}) should be possible, which would make implementing this a little bit more complicated.

Elixir already has ~D so if there's ever an interpolating variant (no such plans as far as I know), that'd be a conflict.

There's also the semantic vs structural comparison:

iex> Decimal.compare Decimal.new("0"), Decimal.new("0.0")
:eq
iex> Decimal.new("0") == Decimal.new("0.0")
false

(edit: below there's perhaps an even more nasty example)

iex> %Decimal{coef: 10, exp: 0, sign: 1}
#Decimal<10>
iex> %Decimal{coef: 1, exp: 1, sign: 1}
#Decimal<1E+1>
iex> Decimal.compare %Decimal{coef: 10, exp: 0, sign: 1}, %Decimal{coef: 1, exp: 1, sign: 1}
:eq

and we'd run into the same issue with pattern matching. People may accidentally shoot themselves in the foot: they would NOT get a match whereas they might expect it.

As much as I personally would like to see this, I'm not sure we should move forward with it.

Good point about the name of the sigil. ~Dec would be better.

I'm not concerned about the feet (maybe because I come from Ruby?). People can already shoot themselves in the foot, 0 == Decimal.new("0") # => false.

About your second example, aren't decimals created using the API normalized?

~Dec would be better.

No multi-letter sigils, sorry! (elixir-lang/elixir#9826 😢)

About your second example, aren't decimals created using the API normalized?

Yes they are and so the expanded code would be too. I think the problem is if you have function with this pattern match but then call it with not normalized one, you'd get potentially unexpected behaviour. It goes back to shooting yourself in the foot again and yeah, I can see both sides of the argument.

~Dec would be better.

No multi-letter sigils, sorry! (elixir-lang/elixir#9826 😢)

Argh 😭 indeed.

So Decimal.d/1 or similar. I only have two weeks of Elixir under my belt so I am not sure what's the best API. Which API did you use in your own test suite?

About your second example, aren't decimals created using the API normalized?

Yes they are and so the expanded code would be too. I think the problem is if you have function with this pattern match but then call it with not normalized one, you'd get potentially unexpected behaviour. It goes back to shooting yourself in the foot again and yeah, I can see both sides of the argument.

Right, so I consider this is a non-issue (there's no excuse for using a struct here, especially a non-normalized one). Having a macro (function or sigil) could only help here by reducing further the use of such structs.

In any case, thanks for the feedback 👍 and feel free to close if it can't/won't be implemented (yet).

We have ~d(0) but because it's custom to our project's test suite, we're not concerned about any future breakage, not that there would likely be any.

I'm going to go ahead and close this but we're happy to continue collecting feedback to potentially revisit it in the future. Thanks!

For what is worth, somewhere in the next months we will have a discussion about complex numbers in Elixir and I want to introduce number suffixes. Such as 1+2i. This means we may be able to have 1.23d. No promises though. Although I would suggest raising if 1.23d is used anywhere in patterns.

@josevalim where did that discussion land?

@wojtekmach numerous times in tests I've wanted to do

assert %SomeStruct{ value: Decimal.new("0.10") } = subject

But this won't work. Instead we are forced to pin

expected_value = Decimal.new("0.10")
assert %SomeStruct{ value: ^expected_value } = subject

It would be nice to do

assert %SomeStruct{ value: ~F[0.10]} = subject

or some other sigil for this purpose.

assert %SomeStruct{ value: ~d[0.10]} = subject

Pattern matching decimals will never be reliable unless they are added as a native type in OTP. This is because the same number can have multiple representations: 1*10^2 = 10*10^1.

That is unfortunate

iex(4)> a = Decimal.new("0.10")
#Decimal<0.10>
iex(5)> b = Decimal.new("0.1000")
#Decimal<0.1000>
iex(6)> ^a = b
** (MatchError) no match of right hand side value: #Decimal<0.1000>