Comparison of Decimals not logical

Try this

Decimal.new(-1) > Decimal.new(0) # true
Decimal.new(-1) < Decimal.new(0) # false

Is this the correct way, I’m using Decimal.compare/2 to work but I would like an explanation

1 Like

I tried, didn’t work. Could you please elaborate which library you are using?

iex(1)> Decimal.new(-1)
** (UndefinedFunctionError) function Decimal.new/1 is undefined (module Decimal is not available)
    Decimal.new(-1)
1 Like

My assumption would be that Decimal.new/1 returns an instance of it’s datatype. Meanwhile Kernel.>/2 simply compares this datatype based on some default rules - i.e. it has no idea what the actual number/value of a Decimal datatype is. So Decimal.compare/2 is the correct way of comparing two Decimals (i.e. don’t think of them as “numbers”).

Looking at the source: decimal.ex line 714

def new(int) when is_integer(int),
    do: %Decimal{sign: (if int < 0, do: -1, else: 1), coef: Kernel.abs(int)}

decimal.ex line 1

defmodule Decimal do
...
  defstruct [sign: 1, coef: 0, exp: 0]

Structs - Elixir

Structs are bare maps underneath

So Kernel.>/2 is comparing two maps - not two decimal values.

2 Likes

I’m in a phoenix project

defp deps do
    [{:phoenix, "~> 1.2.0-rc.0"},
     {:postgrex, ">= 0.11.1"},
     {:phoenix_pubsub, "~> 1.0.0-rc"},
     {:phoenix_ecto, "~> 3.0-rc.0"},
     {:phoenix_html, "~> 2.5.1"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"},
     {:poison, "~> 2.1", override: true}, #{:poison, "~> 1.5"},
     {:comeonin, "~> 2.4.0"},
     {:guardian, "~> 0.10.1"},
     {:credo, "~> 0.3", only: [:dev, :test]},
     {:ex_machina, "~> 1.0.0-beta.1", github: "thoughtbot/ex_machina", only: :test}
   ]
  end
1 Like

postgrex -> decimal
phoenix_ecto -> ecto -> decimal

2 Likes

Then the module Decimal is propably from package decimal, which is a dependenciy of opostgrex and phoenix_ecto.

And in fact, the new/1 function of that module is returning a struct, which again are implemented using erlang maps.

Erlang does describe the comparison of a map like this:

link

So after we already know the definition of Decimal.new/1 is def new(int) when is_integer(int), do: %Decimal{sign: (if int < 0, do: -1, else: 1), coef: Kernel.abs(int)}, we can also tell that we will get the following maps back:

  • Decimal.new(-1) #=> %{__struct__: Decimal, sign: -1, coef: 1, exp: 0}
  • Decimal.new(0) #=> %{__struct__: Decimal, sign: 0, coef: 0, exp: 0}

Now comparing them:

  1. Ordered by size: They have the same size
  2. Ordered by keys: the keys are the same
  3. Ordered by values (comapred in order [:__struct__, :coef, :exp, :sign])
  4. __struct__: equal!
  5. :coef: Comparing 1 and 0, BEAM comes to the conclusion that 1 is greater than 0 and will return an appropriate answer to your question.
  6. It does not care anymore for :exp and :sign
4 Likes

Your assumption is correct.

The Decimal library that is used by Ecto (see it for yourself using mix deps.tree) represents decimals as Structs.

Internally, structs are represented as maps. The comparison operators work, for historical reasons, for all basic datatypes. (Even comparing different datatypes with each other is supported, i.e. comparing a number to a list or a tuple to a map. For the curious: number < atom < reference < fun < port < pid < tuple < map < list < bit string)

This is an (arguably ugly) implementation detail of Erlang. Code we write using a data type from some library should not care about the internal structure of that data type, i.e. we should treat it as a ‘black box’ (this improves code decoupling).

However, as a Decimal is supposed to be a number, it would be nice to compare it to all other kinds of numbers. But here another (arguably ugly) internal implementation detail of Erlang comes up: Operators like > are allowed inside Guard clauses, but only since they are on a very small white-list of ‘guard-safe built-in functions’. When we redefine >, this is no longer possible.

This is the reason that libraries like Decimal and Timex use functions like *.compare/2 instead of > and < for comparisons. I really hope that this will change in the future (for instance, if Erlang introduces guard-safe short-circuiting boolean-logic operators we could build many custom guard-safe operators), as it feels very Java-esque to me.

Tl;Dr: For the time being, you’ll have to use *.compare/2 to compare custom data types.

4 Likes

Wow thanks a lot you really care!! :heart_eyes:

2 Likes

@Nobbz Interesting! This would suggest that if we reorder the Struct fields in Decimal, we could make the comparison operators work:

defmodule EpicDecimal do
   defstruct "1sign": 0, "2exp": 0, "3coef": 0
end

x = %EpicDecimal{"3coef": 0}
y = %EpicDecimal{"3coef": 3}
z = %EpicDecimal{"3coef": 3, "1sign": -1}
z < x && x < y

But this is probably a horrible idea for some other reasons ^^’. (one thing I can think of is, because the sign is separate, that -0 < 0, which should be equal)

1 Like

If you are comparing Decimals to Decimals that would really work yes, but Decimal.new(1) < 0 would still return false, since integer is always greater than a map…

2 Likes

It took me a bit by surprise that Decimal.compare/2 returns Decimal<-1>, Decimal<0>, or Decimal<1>. I guess it makes sense that you want to stay within the same domain and you view compare as sgn(a - b) (Sign function). However coming from other languages one might be primed to expect integer -1, 0, or 1.

Using Decimal.cmp/2 returning :lt, :eq, or :gt may actually be clearer.

2 Likes

O_o… that is very odd indeed. I would expect any function named compare to indeed return integers, similar to for instance the UFO-operator that Rubyists might be familiar with.

I actually think that a built-in Decimal datatype would be a great addition to both Elixir and Erlang.

1 Like

The current way that > works with the Decimal Struct is just a side effect of the implementation of Maps. Below a certain number of entries the order of Map.keys is fixed, above that it can vary since the underlying implementation switches and Map.keys no longer returns a fixed order.

FWIW, this is a place where I think Structs are a suboptimal solution. With something this small a tuple would make more sense and with some care in construction would work “better” with normal Erlang/Elixir operators.

I guess the intent is to make a data type that can be used for money.

4 Likes

Having complete comparison over all terms is actually a very nice feature that allows us to implement data structures such as ordered dictionaries very easily.

We could implement such functions if we could access map keys in guards.

It wouldn’t work because of the exponent unfortunately.

Not sure I understand. Why would you expect compare/2 to return integers but not cmp/2? The reason compare/2 returns a Decimal is mostly because the library follows the IEEE754 specification.

Can you show how it would work better with tuples? One of the reasons it’s a struct is because we can implement protocols for it so it works with JSON encoding, inspecting, converting to string and so on.

4 Likes

I’m probably speaking out of turn, I was only considering the comparability aspect.

But if it was a simple tuple of say { sign, exponent, coefficient } then the Erlang comparision operators would “work” for at least positive numbers and it should be possible to design the tuple so it works for all numbers.

I’d have to dig much deeper into Decimal to know if this tradeoff makes any sense in the larger context of all the things Decimal does. A quick look suggests my assumptions about what the library actually does may be wrong.

1 Like

I did some more looking and thinking and using Tuples for Decimal to enable “<” use would only work if you always normalize the coefficient and somehow put leading zeros in front of it.

You could add a number one order of magnitude larger than the specified precision to fake this, but that would complicate all the rest of the operators.

Another approach would be to use actual binaries for the coefficient, but that also adds a lot of
complexity.

2 Likes

Aha! I had not considered that. Thank you, I’ve been wondering about the actual reasoning behind the ‘all things are comparable’ in Erlang for a very long time. :slight_smile:

:thumbsup:

Yes, you’re right. I had not considered that (1 * 10^3) < (123 * 10^1)

If you follow the IEEE specification, then it makes sense to return Decimals there. It is just that I would expect comparing functions (like compare/2 or cmp/2) to be used in checks like check if a is larger or equal to b. compare(a, b) >= 0. Of course this no different from checking if b is strictly smaller than a, but depending on the context it is often more readable to use a x >= y over a y < x or a x <= y over a y > x.

So that would be a reason for a comparing function to return integers instead of decimals or symbols. Of course, if a library were to offer functions like lte?(a, b), that is a great way too.


Oh, and let me just say that I really like the Decimal library :smiley:

1 Like

Recently I wrote simple, experimental library which makes dealing with Decimal library a bit easier. It is only a thin wrapper so the actual calculations are made by Decimal anyway.

1 Like

FWIW, I’ve come up with a basic tuple implementation for Decimal-like library that does work with “<”. It’s about as complex as I expected although the trick is to fiddle the exponent, not the mantissa or significand.

1 Like

Can someone explain the problem Decimal is meant to solve that isn’t solved by float? Or just exactly what the use case is for Decimal?

I understand that it can do things float can not, I’m just trying to grasp the itch this library scratches. I think you could tweak it such that < would work, but the data structure I have come
up with is heavily dependent on the precision requested.

Would doing that break the entire purpose of the library?

My guess is that it’s important that the data structure represent the number w/o additional context provided by Decimal in the process table. The representation I have come up with has the precision as a deductible fact, but it’s tricky to work with items that were build with different notions of precision.

1 Like