# 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
2 Likes

Then the module `Decimal` is propably from package `decimal`, which is a dependenciy of o`postgrex` 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:

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!! 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 `Decimal`s to `Decimal`s 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.  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 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