Skip to content
Protocol which describes symmetric equivalence relation for pair of types
Elixir Shell
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
config
lib docs Mar 25, 2019
priv/img
scripts add dev tools Feb 24, 2019
test protocol is implemented for standard Erlang types Feb 24, 2019
.credo.exs add dev tools Feb 24, 2019
.dialyzer_ignore
.editorconfig add dev tools Feb 24, 2019
.formatter.exs add dev tools Feb 24, 2019
.gitignore init Feb 23, 2019
CHANGELOG.md v0.1.1 Mar 25, 2019
README.md
VERSION v0.1.1 Mar 25, 2019
coveralls.json protocol is implemented for standard Erlang types Feb 24, 2019
mix.exs description Mar 25, 2019
mix.lock add dev tools Feb 24, 2019

README.md

Equalable

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).

Hex Documentation

Installation

The package can be installed by adding equalable to your list of dependencies in mix.exs:

def deps do
  [
    {:equalable, "~> 0.1.0"}
  ]
end

Motivation

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.

Example

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

Utilities

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
You can’t perform that action at this time.