2

Consider I have a map %{} where I have keys that are Decimal.

Problematically, in Decimal 3 != 3.0, so as a result, indexing on Decimal keys is unreliable, and requires the use of Decimal.eq?/2 to test equality.

Is there a way I can overload the map subscription operator, in order that indexing on Decimal actually uses eq? instead ==?

i.e. mymap[Decimal.new(3)] == mymap[Decimal.from_float(3.0)]

If there's another approach I'm missing (e.g. Protocols/macros/something else, please let me know!)

cjm2671
  • 18,348
  • 31
  • 102
  • 161

2 Answers2

1

Unfortunately, even if Elixir offers many ways to extend the language (protocols/macros...), maps are an underlying construct provided by the Virtual Machine and you won't be able to override this behavior. Maps need to rely on some hashing of keys, not just a comparison function.

What should work for your use case is to normalize your keys first (before put and access) using Decimal.normalize/1, so that the key is the same for both:

iex> Decimal.new(3) |> Decimal.normalize()
#Decimal<3>
iex> Decimal.from_float(3.0) |> Decimal.normalize()
#Decimal<3>

You can probably wrap this in a module for frequent operations:

defmodule DecimalMap do
  def put(map, key, value), do: Map.put(map, Decimal.normalize(key), value)
  def fetch(map, key), do: Map.fetch(map, Decimal.normalize(key))
  # ...
end
iex> map = DecimalMap.put(%{}, Decimal.from_float(3.0), "hello")
%{#Decimal<3> => "hello"}
iex> DecimalMap.fetch(map, Decimal.new(3))
{:ok, "hello"}
sabiwara
  • 2,775
  • 6
  • 11
1

I think there are a few misconceptions here.

  1. There's no such thing as the "subscription operator" as far as I know. If you are referring to [], this is knowns as the Access syntax.
  2. "indexing on Decimal keys is unreliable". Decimal is a struct, and structs usually come with additional semantics, meaning that you cannot rely on the underlying underlying shape of the struct, you have to call the functions in the module (eq?) to extract meaning. If you are using a Decimal as a map key, you are implicitly relying on its structure rather semantics, which is not valid in this case, because different Decimal structs can be considered semantically equivalent.
  3. Decimal.from_float(3.0) is not a precise operation due to the imprecise nature of floats. For example Decimal.from_float(0.3) != Decimal.from_float(0.1 + 0.2). If your data is coming from floats, you basically cannot compare them for exact equality, because they are not precise, thus should neither be using them as keys, nor comparing them with equality operators such as == or Decimal.eq?.

If you just wanted to consider equivalent decimals as equal (resolving point 2 above, but not point 3), you could normalize them before putting them in the map:

Decimal.normalize(Decimal.new("3")) == Decimal.normalize(Decimal.new("3.0"))

However, this is a dangerous operation if you are generating the decimals from other data, because different Decimal structs are considered equivalent. For example, consider the following map:

map = %{Decimal.new("3") => "three", Decimal.new("3.0") => "three point zero"}

If we normalize the keys, we lose one of the unique values and get counter-intuitive responses:

Map.new(map, fn {k, v} -> {Decimal.normalize(k), v} end)
%{#Decimal<3> => "three point zero"}
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74