Protocol which describes symmetric equivalence relation for pair of types. There are cases where we want to define equivalence relation between two terms not just using term values according standard Erlang/Elixir equivalence rules but to use some meaningful business logic to do it. Main purpose of this package is to provide extended versions of standard Kernel functions like ==/2
and !=/2
which will rely on Equalable protocol implementation for given pair of types. Protocol itself is pretty similar to Eq Haskell type class (but can be applied to pair of values of different types as well).
The package can be installed by adding equalable
to your list of dependencies in mix.exs
:
def deps do
[
{:equalable, "~> 0.1.0"}
]
end
Kernel ==/2
function work pretty fine with standard numeric types like integer or float (and it works even in nested terms like map):
iex> %{a: 1} == %{a: 1.0}
true
But if we try to apply Kernel ==/2
function to terms containing custom Decimal numbers it will not work so good:
iex(1)> %{a: Decimal.new("1")} == %{a: Decimal.new("1.0")}
false
This is because the same decimal number can be presented as different Elixir term:
iex> Decimal.new("1") |> Map.from_struct
%{coef: 1, exp: 0, sign: 1}
iex> Decimal.new("1.0") |> Map.from_struct
%{coef: 10, exp: -1, sign: 1}
And here Equalable protocol can help us.
Let's implement equivalence relation between Decimal and Integer, Float and BitString types using existing Decimal.equal?/2
helper:
use Eq
defequalable left :: Decimal, right :: Decimal do
Decimal.equal?(left, right)
end
defequalable left :: Integer, right :: Decimal do
left
|> Decimal.new()
|> Decimal.equal?(right)
end
defequalable left :: Float, right :: Decimal do
left
|> Decimal.from_float()
|> Decimal.equal?(right)
end
defequalable left :: BitString, right :: Decimal do
left
|> Decimal.new()
|> Decimal.equal?(right)
end
And then we can use Eq.equal?/2
utility function instead of Kernel ==/2
:
iex> Eq.equal?(Decimal.new("1"), Decimal.new("1.0"))
true
iex> Eq.equal?(Decimal.new("1.0"), Decimal.new("1"))
true
iex> Eq.equal?(Decimal.new("1"), 1)
true
iex> Eq.equal?(1, Decimal.new("1"))
true
iex> Eq.equal?(Decimal.new("1"), 1.0)
true
iex> Eq.equal?(1.0, Decimal.new("1"))
true
iex> Eq.equal?(Decimal.new("1"), "1.0")
true
iex> Eq.equal?("1.0", Decimal.new("1"))
true
iex> Eq.equal?("1.0", Decimal.new("1.1"))
false
which works as expected according meaning of Decimal numbers instead of just term values. Equivalence relation based on Eualable protocol is very useful when for example we compare big nested structures which contain Decimals or other custom types (like Date, Time, NaiveDateTime, URI etc) in nested collections like lists, maps, tuples or other data types:
iex> x0 = %{a: [%{b: Decimal.new("1")}]}
%{a: [%{b: #Decimal<1>}]}
iex> x1 = %{a: [%{b: Decimal.new("1.0")}]}
%{a: [%{b: #Decimal<1.0>}]}
iex> x0 == x1
false
iex> Eq.equal?(x0, x1)
true
If Equalable protocol is not defined for pair of given types then Eq.equal?/2
function fallbacks to Kernel ==/2
:
iex> x0 = URI.parse("https://hello.world")
%URI{
authority: "hello.world",
fragment: nil,
host: "hello.world",
path: nil,
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}
iex> x1 = "https://hello.world"
"https://hello.world"
iex> x0 == x1
false
iex> Eq.equal?(x0, x1)
false
Eq
module provides utilities and infix shortcuts for equivalence relation:
Kernel.fn/2 | Eq.fn/2 | Eq infix shortcut |
---|---|---|
x == y | Eq.equal?(x, y) | x <~> y |
x != y | Eq.not_equal?(x, y) | x <|> y |
Example of infix shortcuts usage:
iex> use Eq
Eq
iex> Decimal.new("1") <~> Decimal.new("1.0")
true
iex> Decimal.new("1.0") <~> Decimal.new("1")
true
iex> 1 <|> 2
true
iex> 2 <|> 1
true