Packages

Comparable (aka Orderable) Elixir protocol

Current section

Files

Jump to
comparable lib comp.ex
Raw

lib/comp.ex

defmodule Comp do
@moduledoc """
Provides utilities to implement and work with `Comparable` types
"""
@type left :: term
@type right :: term
defmacro gt, do: :gt
defmacro lt, do: :lt
defmacro eq, do: :eq
@doc """
Enables `<~>`, `<|>`, `>>>`, `<<<`, `~>>`, `<<~` infix shortcuts and
`Comp.gt`, `Comp.lt`, `Comp.eq`, `defcomparable`, `gen_ne_test`, `gen_eq_test` macro
"""
defmacro __using__(_) do
quote do
require Comp
import Comp,
only: [
defcomparable: 3,
<~>: 2,
<|>: 2,
>>>: 2,
<<<: 2,
~>>: 2,
<<~: 2,
gen_ne_test: 3,
gen_eq_test: 3
]
end
end
@doc """
Helper to define ordering relation for pair of types,
accepts two `term :: type` pairs
and block of code where relation is described.
## Examples
```
iex> quote do
...> use Comp
...> defmodule Foo do
...> defstruct [:value, :meta]
...> end
...> defmodule Bar do
...> defstruct [:value, :meta]
...> end
...> defcomparable %Foo{value: left} :: Foo, %Foo{value: right} :: Foo do
...> Comp.compare(left, right)
...> end
...> defcomparable %Foo{value: left} :: Foo, %Bar{value: right} :: Bar do
...> Comp.compare(left, right)
...> end
...> defcomparable %Foo{value: left} :: Foo, right :: Integer do
...> Comp.compare(left, right)
...> end
...> end
...> |> Code.compile_quoted
iex> quote do
...> x = %Foo{value: 1, meta: 1}
...> y = %Foo{value: 1, meta: 2}
...> Comp.equal?(x, y) && Comp.equal?(y, x)
...> end
...> |> Code.eval_quoted
...> |> elem(0)
true
iex> quote do
...> x = %Foo{value: 1, meta: 1}
...> y = %Bar{value: 1, meta: 2}
...> Comp.equal?(x, y) && Comp.equal?(y, x)
...> end
...> |> Code.eval_quoted
...> |> elem(0)
true
iex> quote do
...> x = %Foo{value: 1, meta: 1}
...> y = 1
...> Comp.equal?(x, y) && Comp.equal?(y, x)
...> end
...> |> Code.eval_quoted
...> |> elem(0)
true
```
"""
defmacro defcomparable(
{:::, _, [left_expression, quoted_left_type]},
{:::, _, [right_expression, quoted_right_type]},
do: code
) do
{left_type, []} = Code.eval_quoted(quoted_left_type, [], __CALLER__)
{right_type, []} = Code.eval_quoted(quoted_right_type, [], __CALLER__)
lr_type =
[Comparable, Type, left_type, To, right_type]
|> Module.concat()
rl_type =
[Comparable, Type, right_type, To, left_type]
|> Module.concat()
lr_impl =
quote do
defmodule unquote(lr_type) do
@fields [:left, :right]
@enforce_keys @fields
defstruct @fields
end
defimpl Comparable, for: unquote(lr_type) do
def compare(%unquote(lr_type){left: unquote(left_expression), right: unquote(right_expression)}) do
unquote(code)
end
end
end
if lr_type == rl_type do
lr_impl
else
quote do
unquote(lr_impl)
defmodule unquote(rl_type) do
@fields [:left, :right]
@enforce_keys @fields
defstruct @fields
end
defimpl Comparable, for: unquote(rl_type) do
def compare(%unquote(rl_type){left: unquote(right_expression), right: unquote(left_expression)}) do
unquote(code)
end
end
end
end
end
defmacro gen_ne_test(name, left, right) do
quote do
test unquote(name) do
x = unquote(left)
y = unquote(right)
refute x <~> y
assert x <|> y
refute x >>> y
assert x <<< y
refute x ~>> y
assert x <<~ y
assert y == Comp.max(x, y)
assert x == Comp.min(x, y)
refute y <~> x
assert y <|> x
assert y >>> x
refute y <<< x
assert y ~>> x
refute y <<~ x
assert y == Comp.max(y, x)
assert x == Comp.min(y, x)
end
end
end
defmacro gen_eq_test(name, left, right) do
quote do
test unquote(name) do
x = unquote(left)
y = unquote(right)
assert x <~> y
refute x <|> y
refute x >>> y
refute x <<< y
assert x ~>> y
assert x <<~ y
assert x == Comp.max(x, y)
assert x == Comp.min(x, y)
assert y <~> x
refute y <|> x
refute y >>> x
refute y <<< x
assert y ~>> x
assert y <<~ x
assert y == Comp.max(y, x)
assert y == Comp.min(y, x)
end
end
end
@doc """
Infix shortcut for `Comp.equal?/2`
## Examples
```
iex> use Comp
Comp
iex> 1 <~> 1
true
iex> 1 <~> :hello
false
```
"""
defmacro left <~> right do
quote do
unquote(left)
|> Comp.equal?(unquote(right))
end
end
@doc """
Infix shortcut for `Comp.not_equal?/2`
## Examples
```
iex> use Comp
Comp
iex> 1 <|> 1
false
iex> 1 <|> :hello
true
```
"""
defmacro left <|> right do
quote do
unquote(left)
|> Comp.not_equal?(unquote(right))
end
end
@doc """
Infix shortcut for `Comp.greater_than?/2`
## Examples
```
iex> use Comp
Comp
iex> 1 >>> 1
false
iex> 1 >>> 2
false
iex> 2 >>> 1
true
```
"""
defmacro left >>> right do
quote do
unquote(left)
|> Comp.greater_than?(unquote(right))
end
end
@doc """
Infix shortcut for `Comp.less_than?/2`
## Examples
```
iex> use Comp
Comp
iex> 1 <<< 1
false
iex> 1 <<< 2
true
iex> 2 <<< 1
false
```
"""
defmacro left <<< right do
quote do
unquote(left)
|> Comp.less_than?(unquote(right))
end
end
@doc """
Infix shortcut for `Comp.greater_or_equal?/2`
## Examples
```
iex> use Comp
Comp
iex> 1 ~>> 1
true
iex> 1 ~>> 2
false
iex> 2 ~>> 1
true
```
"""
defmacro left ~>> right do
quote do
unquote(left)
|> Comp.greater_or_equal?(unquote(right))
end
end
@doc """
Infix shortcut for `Comp.less_or_equal?/2`
## Examples
```
iex> use Comp
Comp
iex> 1 <<~ 1
true
iex> 1 <<~ 2
true
iex> 2 <<~ 1
false
```
"""
defmacro left <<~ right do
quote do
unquote(left)
|> Comp.less_or_equal?(unquote(right))
end
end
@doc """
Is left term equal to right term?
## Examples
```
iex> Comp.equal?(1, 1)
true
iex> Comp.equal?(1, :hello)
false
```
"""
@spec equal?(left, right) :: boolean
def equal?(left, right) do
left
|> new(right)
|> Comparable.compare() == eq()
end
@doc """
Is left term not equal to right term?
## Examples
```
iex> Comp.not_equal?(1, 1)
false
iex> Comp.not_equal?(1, :hello)
true
```
"""
@spec not_equal?(left, right) :: boolean
def not_equal?(left, right) do
left
|> new(right)
|> Comparable.compare() != eq()
end
@doc """
Is left term greater than right term?
## Examples
```
iex> Comp.greater_than?(1, 1)
false
iex> Comp.greater_than?(1, 2)
false
iex> Comp.greater_than?(2, 1)
true
"""
@spec greater_than?(left, right) :: boolean
def greater_than?(left, right) do
left
|> new(right)
|> Comparable.compare() == gt()
end
@doc """
Is left term less than right term?
## Examples
```
iex> Comp.less_than?(1, 1)
false
iex> Comp.less_than?(1, 2)
true
iex> Comp.less_than?(2, 1)
false
"""
@spec less_than?(left, right) :: boolean
def less_than?(left, right) do
left
|> new(right)
|> Comparable.compare() == lt()
end
@doc """
Is left term greater or equal to right term?
## Examples
```
iex> Comp.greater_or_equal?(1, 1)
true
iex> Comp.greater_or_equal?(1, 2)
false
iex> Comp.greater_or_equal?(2, 1)
true
"""
@spec greater_or_equal?(left, right) :: boolean
def greater_or_equal?(left, right) do
left
|> new(right)
|> Comparable.compare() != lt()
end
@doc """
Is left term less or equal to right term?
## Examples
```
iex> Comp.less_or_equal?(1, 1)
true
iex> Comp.less_or_equal?(1, 2)
true
iex> Comp.less_or_equal?(2, 1)
false
"""
@spec less_or_equal?(left, right) :: boolean
def less_or_equal?(left, right) do
left
|> new(right)
|> Comparable.compare() != gt()
end
@doc """
Returns the biggest of the two given terms, if terms are equal - then the first one is returned
## Examples
```
iex> Comp.max(1, 1)
1
iex> Comp.max(1, 2)
2
iex> Comp.max(2, 1)
2
```
"""
@spec max(left, right) :: left | right
def max(left, right) do
left
|> new(right)
|> Comparable.compare()
|> case do
gt() -> left
lt() -> right
eq() -> left
end
end
@doc """
Returns the smallest of the two given terms, if terms are equal - then the first one is returned
## Examples
```
iex> Comp.min(1, 1)
1
iex> Comp.min(1, 2)
1
iex> Comp.min(2, 1)
1
```
"""
@spec min(left, right) :: left | right
def min(left, right) do
left
|> new(right)
|> Comparable.compare()
|> case do
gt() -> right
lt() -> left
eq() -> left
end
end
@doc """
Compare left and right term
## Examples
```
iex> Comp.compare(1, 2)
:lt
iex> Comp.compare(2, 1)
:gt
iex> Comp.compare(1, 1)
:eq
```
"""
@spec compare(left, right) :: Comparable.ord()
def compare(left, right) do
left
|> new(right)
|> Comparable.compare()
end
defp new(left, right) do
lr_type =
try do
[Comparable, Type, Typable.type_of(left), To, Typable.type_of(right)]
|> Module.safe_concat()
rescue
ArgumentError ->
[Comparable, Type, Any, To, Any]
|> Module.safe_concat()
end
%{__struct__: lr_type, left: left, right: right}
end
end