defmodule Cmp do
@moduledoc """
Semantic comparison and sorting for Elixir.
## Why `Cmp`?
The built-in comparison operators as well as functions like `Enum.sort/2` or `Enum.max/1`
are based on Erlang's term ordering and suffer two issues, which require attention and
might lead to unexpected behaviors or bugs:
### 1. Structural comparisons
Built-ins use [structural comparison over semantic comparison](
https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
~D[2020-03-02] > ~D[2019-06-06]
false
iex> Enum.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
[~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]
Semantic comparison is available but not straightforward:
iex> Date.compare(~D[2019-01-01], ~D[2020-03-02])
:lt
iex> Enum.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]], Date)
[~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]
`Cmp` does the right thing out of the box:
iex> Cmp.gt?(~D[2020-03-02], ~D[2019-06-06])
true
iex> Cmp.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
[~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]
### 2. Weakly typed
Built-in comparators accept any set of operands:
iex> 2 < "1"
true
iex> 0 < true
true
iex> false < nil
true
`Cmp` will only compare compatible elements or raise a `Cmp.TypeError`:
iex> Cmp.lte?(1, 1.0)
true
iex> Cmp.lte?(2, "1")
** (Cmp.TypeError) Failed to compare incompatible types - left: 2, right: "1"
## What's in the box
- Boolean comparisons: `eq?/2`, `lt?/2`, `gt?/2`, `lte?/2`, `gte?/2`
- Equivalents of `Kernel.min/2`/ `Kernel.max/2`: `Cmp.min/2`, `Cmp.max/2`
- Equivalents of `Enum.min/1`/ `Enum.max/1`/`Enum.sort/2`: `Cmp.min/1`, `Cmp.max/1`, `Cmp.sort/2`
- `compare/2`
## Supported types
The `Cmp.Comparable` protocol is implemented for the following types:
- `Integer`
- `Float`
- `Bitstring`
- `Date`
- `Time`
- `DateTime`
- `NaiveDateTime`
- `Version`
- `Tuple` (see below)
- `Decimal` (if available)
It isn't implemented for atoms by design, since atoms are not semantically
an ordered type.
It supports tuples of the same size and types:
iex> Cmp.max({12, ~D[2019-06-06]}, {12, ~D[2020-03-02]})
{12, ~D[2020-03-02]}
iex> Cmp.max({12, "Foo"}, {15, nil})
** (Cmp.TypeError) Failed to compare incompatible types - left: "Foo", right: nil
iex> Cmp.max({12, "Foo"}, {15})
** (Cmp.TypeError) Failed to compare incompatible types - left: {12, "Foo"}, right: {15}
`Decimal` support can prevent nasty bugs too:
iex> max(Decimal.new(2), Decimal.from_float(1.0))
#Decimal<1.0>
iex> Cmp.max(Decimal.new(2), Decimal.from_float(1.0))
#Decimal<2>
See the `Cmp.Comparable` documentation to implement the protocol for other existing
or new structs.
## Design goals
- Fast and well-optimized - the overhead should be quite small over built-in equivalents.
See the `benchmarks/` folder for more details.
- No need to require macros, plain functions
- Easily extensible through the `Cmp.Comparable` protocol
- Robust and well-tested (both unit and property-based)
Supporting comparisons between non-homogeneous types such as mixed `Decimal` and
built-in numbers for instance is a non-goal. This limitation is a necessary
trade-off in order to ensure the points above. Use the `Decimal` library
directly if you need this.
## Limitations
- `Cmp` comparators cannot be used in guards.
- `Cmp` does not support (or plan to support) comparisons between non-homogeneous types
(e.g. `Decimal` and native numbers).
"""
alias Cmp.Comparable
alias Cmp.Util
@compile :inline_list_funcs
defguardp is_base_type(value) when is_number(value) or is_binary(value)
defguardp is_same_base_type(left, right)
when (is_number(left) and is_number(right)) or
(is_binary(left) and is_binary(right))
@doc """
Safe equivalent to `>/2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.gt?(2, 1)
true
iex> Cmp.gt?(1, 2)
false
iex> Cmp.gt?(1, 1.0)
false
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.gt?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Unlike `>/2`, it will perform a semantic comparison for structs and not a
[structural comparison](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
iex> Cmp.gt?(~D[2020-03-02], ~D[2019-06-06])
true
~D[2020-03-02] > ~D[2019-06-06]
false
"""
@spec gt?(Comparable.t(), Comparable.t()) :: boolean()
def gt?(left, right) when is_same_base_type(left, right), do: left > right
def gt?(left, right), do: Comparable.compare(left, right) == :gt
@doc """
Safe equivalent to `>=/2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.gte?(2, 1)
true
iex> Cmp.gte?(1, 2)
false
iex> Cmp.gte?(1, 1.0)
true
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.gte?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Unlike `>=/2`, it will perform a semantic comparison for structs and not a
[structural comparison](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
iex> Cmp.gte?(~D[2020-03-02], ~D[2019-06-06])
true
~D[2020-03-02] >= ~D[2019-06-06]
false
"""
@spec gte?(Comparable.t(), Comparable.t()) :: boolean()
def gte?(left, right) when is_same_base_type(left, right), do: left >= right
def gte?(left, right), do: Comparable.compare(left, right) != :lt
@doc """
Safe equivalent to `</2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.lt?(1, 2)
true
iex> Cmp.lt?(2, 1)
false
iex> Cmp.lt?(1, 1.0)
false
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.lt?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Unlike `</2`, it will perform a semantic comparison for structs and not a
[structural comparison](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
iex> Cmp.lt?(~D[2019-06-06], ~D[2020-03-02])
true
~D[2019-06-06] < ~D[2020-03-02]
false
"""
@spec lt?(Comparable.t(), Comparable.t()) :: boolean()
def lt?(left, right) when is_same_base_type(left, right), do: left < right
def lt?(left, right), do: Comparable.compare(left, right) == :lt
@doc """
Safe equivalent to `<=/2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.lte?(1, 2)
true
iex> Cmp.lte?(2, 1)
false
iex> Cmp.lte?(1, 1.0)
true
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.lte?(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Unlike `<=/2`, it will perform a semantic comparison for structs and not a
[structural comparison](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
iex> Cmp.lte?(~D[2019-06-06], ~D[2020-03-02])
true
~D[2019-06-06] <= ~D[2020-03-02]
false
"""
@spec lte?(Comparable.t(), Comparable.t()) :: boolean()
def lte?(left, right) when is_same_base_type(left, right), do: left <= right
def lte?(left, right), do: Comparable.compare(left, right) != :gt
@doc """
Safe equivalent to `==/2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.eq?(2, 1)
false
iex> Cmp.eq?(1, 1.0)
true
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.eq?(1, "")
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: ""
"""
@spec eq?(Comparable.t(), Comparable.t()) :: boolean()
def eq?(left, right) when is_same_base_type(left, right), do: left == right
def eq?(left, right), do: Comparable.compare(left, right) == :eq
@doc """
Returns `:gt` if `left` is semantically greater than `right`, `lt` if `left`
is less than `right`, and `:eq` if they are equal.
## Examples
iex> Cmp.compare(2, 1)
:gt
iex> Cmp.compare(1, 1.0)
:eq
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.compare(1, "")
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: ""
"""
@spec compare(Comparable.t(), Comparable.t()) :: :eq | :lt | :gt
def compare(left, right) when is_same_base_type(left, right),
do: Util.compare_terms(left, right)
def compare(left, right), do: Comparable.compare(left, right)
@doc """
Safe equivalent to `max/2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.max(1, 2)
2
iex> Cmp.max(1, 1.0)
1
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.max(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Unlike `max/2`, it will perform a semantic comparison for structs and not a
[structural comparison](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
iex> Cmp.max(~D[2020-03-02], ~D[2019-06-06])
~D[2020-03-02]
max(~D[2020-03-02], ~D[2019-06-06])
~D[2019-06-06]
"""
@spec max(value, value) :: value when value: Comparable.t()
def max(left, right) when is_same_base_type(left, right), do: Kernel.max(left, right)
def max(left, right) do
case Comparable.compare(left, right) do
:lt -> right
_ -> left
end
end
@doc """
Safe equivalent to `min/2`, which only works if both types are compatible and
uses semantic comparison.
## Examples
iex> Cmp.min(2, 1)
1
iex> Cmp.min(1, 1.0)
1
It will raise a `Cmp.TypeError` if trying to compare incompatible types
iex> Cmp.min(1, nil)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Unlike `min/2`, it will perform a semantic comparison for structs and not a
[structural comparison](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
iex> Cmp.min(~D[2020-03-02], ~D[2019-06-06])
~D[2019-06-06]
min(~D[2020-03-02], ~D[2019-06-06])
~D[2020-03-02]
"""
@spec min(value, value) :: value when value: Comparable.t()
def min(left, right) when is_same_base_type(left, right), do: Kernel.min(left, right)
def min(left, right) do
case Comparable.compare(left, right) do
:gt -> right
_ -> left
end
end
@doc """
Safe equivalent of `Enum.sort/2`.
## Examples
iex> Cmp.sort([3, 1, 2])
[1, 2, 3]
iex> Cmp.sort([3, 1, 2], :desc)
[3, 2, 1]
Respects semantic comparison:
iex> Cmp.sort([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
[~D[2019-01-01], ~D[2019-06-06], ~D[2020-03-02]]
Raises a `Cmp.TypeError` on non-uniform enumerables:
iex> Cmp.sort([~D[2019-01-01], nil, ~D[2020-03-02]])
** (Cmp.TypeError) Failed to compare incompatible types - left: ~D[2019-01-01], right: nil
"""
@spec sort(Enumerable.t(elem), :asc | :desc) :: Enumerable.t(elem) when elem: Comparable.t()
def sort(enumerable, order \\ :asc)
def sort([head | tail] = list, order) when is_base_type(head) do
case order do
:asc ->
check_list(head, tail)
:lists.sort(list)
:desc ->
[head | tail] = :lists.sort(list)
check_reverse(head, tail, [])
end
end
for struct <- Util.comparable_structs() do
module = Module.concat(Cmp.Comparable, struct)
def sort([%unquote(struct){} | _] = list, order) do
gt_or_lt =
case order do
:asc -> :gt
:desc -> :lt
end
:lists.sort(&(unquote(module).compare(&1, &2) != gt_or_lt), list)
end
end
def sort([head | _] = list, order) do
gt_or_lt =
case order do
:asc -> :gt
:desc -> :lt
end
module = Comparable.impl_for!(head)
:lists.sort(&(module.compare(&1, &2) != gt_or_lt), list)
end
def sort(list, :asc) when is_list(list) do
:lists.sort(<e?/2, list)
end
def sort(list, :desc) when is_list(list) do
:lists.sort(>e?/2, list)
end
def sort(enumerable, :asc) do
Enum.sort(enumerable, <e?/2)
end
def sort(enumerable, :desc) do
Enum.sort(enumerable, >e?/2)
end
defp check_list(_, []), do: :ok
defp check_list(prev, [head | tail]) when is_same_base_type(prev, head) do
check_list(head, tail)
end
defp check_list(prev, [head | _tail]) do
raise Cmp.TypeError, left: prev, right: head
end
defp check_reverse(last, [], acc), do: [last | acc]
defp check_reverse(prev, [head | tail], acc) when is_same_base_type(prev, head) do
check_reverse(head, tail, [prev | acc])
end
defp check_reverse(prev, [head | _tail], _acc) do
raise Cmp.TypeError, left: head, right: prev
end
@doc """
Safe equivalent of `Enum.max/2`, returning the minimum of a non-empty
enumerable of comparables.
## Examples
iex> Cmp.max([1, 3, 2])
3
Respects semantic comparison:
iex> Cmp.max([~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]])
~D[2020-03-02]
Raises a `Cmp.TypeError` on non-uniform enumerables:
iex> Cmp.max([1, nil, 2])
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Raises an `Enum.EmptyError` on empty enumerables:
iex> Cmp.max([])
** (Enum.EmptyError) empty error
"""
@spec max(Enumerable.t(elem)) :: elem when elem: Comparable.t()
def max(enumerable)
def max([head | tail]) when is_number(head), do: max_list_numbers(tail, head)
def max([head | tail]) when is_binary(head), do: max_list_binaries(tail, head)
def max([head | _] = list) do
module = Comparable.impl_for!(head)
Enum.max(list, &(module.compare(&1, &2) != :lt))
end
def max(enumerable) do
Enum.max(enumerable, >e?/2)
end
@compile {:inline, max_list_numbers: 2, max_list_binaries: 2}
for {fun, guard} <- [
max_list_numbers: :is_number,
max_list_binaries: :is_binary
] do
defp unquote(fun)([], acc), do: acc
defp unquote(fun)([head | tail], acc) when unquote(guard)(head) do
acc =
case head do
new_val when new_val > acc -> new_val
_ -> acc
end
unquote(fun)(tail, acc)
end
defp unquote(fun)([head | _tail], acc) do
raise Cmp.TypeError, left: acc, right: head
end
end
@doc """
Safe equivalent of `Enum.min/2`, returning the maximum of a non-empty
enumerable of comparables.
## Examples
iex> Cmp.min([1, 3, 2])
1
Respects semantic comparison:
iex> Cmp.min([~D[2020-03-02], ~D[2019-06-06]])
~D[2019-06-06]
Raises a `Cmp.TypeError` on non-uniform enumerables:
iex> Cmp.min([1, nil, 2])
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Raises an `Enum.EmptyError` on empty enumerables:
iex> Cmp.min([])
** (Enum.EmptyError) empty error
"""
@spec min(Enumerable.t(elem)) :: elem when elem: Comparable.t()
def min(enumerable)
def min([head | tail]) when is_number(head), do: min_list_numbers(tail, head)
def min([head | tail]) when is_binary(head), do: min_list_binaries(tail, head)
def min([head | _] = list) do
module = Comparable.impl_for!(head)
Enum.min(list, &(module.compare(&1, &2) != :gt))
end
def min(enumerable) do
Enum.min(enumerable, <e?/2)
end
@compile {:inline, min_list_numbers: 2, min_list_binaries: 2}
for {fun, guard} <- [
min_list_numbers: :is_number,
min_list_binaries: :is_binary
] do
defp unquote(fun)([], acc), do: acc
defp unquote(fun)([head | tail], acc) when unquote(guard)(head) do
acc =
case head do
new_val when new_val < acc -> new_val
_ -> acc
end
unquote(fun)(tail, acc)
end
defp unquote(fun)([head | _tail], acc) do
raise Cmp.TypeError, left: acc, right: head
end
end
@doc """
Safe equivalent of `Enum.sort_by/2`.
## Examples
iex> Cmp.sort_by([%{x: 3}, %{x: 1}, %{x: 2}], & &1.x)
[%{x: 1}, %{x: 2}, %{x: 3}]
iex> Cmp.sort_by([%{x: 3}, %{x: 1}, %{x: 2}], & &1.x, :desc)
[%{x: 3}, %{x: 2}, %{x: 1}]
Respects semantic comparison:
iex> Cmp.sort_by([%{date: ~D[2020-03-02]}, %{date: ~D[2019-06-06]}], & &1.date)
[%{date: ~D[2019-06-06]}, %{date: ~D[2020-03-02]}]
Raises a `Cmp.TypeError` on non-uniform enumerables:
iex> Cmp.sort_by([%{x: 3}, %{x: "1"}, %{x: 2}], & &1.x)
** (Cmp.TypeError) Failed to compare incompatible types - left: 3, right: "1"
"""
@spec sort_by(Enumerable.t(elem), (elem -> Comparable.t()), :asc | :desc) :: Enumerable.t(elem)
when elem: term()
def sort_by(enumerable, fun, order \\ :asc)
when is_function(fun, 1) and order in [:asc, :desc] do
enumerable
|> Enum.to_list()
|> sort_by_prepare(fun)
|> unwrap_sort_by(order)
end
defp sort_by_prepare([], _fun), do: []
defp sort_by_prepare([head | tail], fun) do
case fun.(head) do
val when is_number(val) ->
sort_by_prepare_numbers(tail, fun, [{val, head}])
val when is_binary(val) ->
sort_by_prepare_binaries(tail, fun, [{val, head}])
val ->
module = Comparable.impl_for!(val)
mapped = [{val, head} | Enum.map(tail, &{fun.(&1), &1})]
:lists.sort(fn {left, _}, {right, _} -> module.compare(left, right) != :gt end, mapped)
end
end
@compile {:inline, sort_by_prepare_numbers: 3}
@compile {:inline, sort_by_prepare_binaries: 3}
@compile {:inline, unwrap_sort_by_desc: 2}
defp sort_by_prepare_numbers([], _fun, acc), do: :lists.keysort(1, acc)
defp sort_by_prepare_numbers([head | tail], fun, acc) do
case fun.(head) do
val when is_number(val) ->
sort_by_prepare_numbers(tail, fun, [{val, head} | acc])
other ->
[{left, _} | _] = acc
raise Cmp.TypeError, left: left, right: other
end
end
defp sort_by_prepare_binaries([], _fun, acc), do: :lists.keysort(1, acc)
defp sort_by_prepare_binaries([head | tail], fun, acc) do
case fun.(head) do
val when is_binary(val) ->
sort_by_prepare_binaries(tail, fun, [{val, head} | acc])
other ->
[{left, _} | _] = acc
raise Cmp.TypeError, left: left, right: other
end
end
defp unwrap_sort_by(list, :asc), do: unwrap_sort_by_asc(list)
defp unwrap_sort_by(list, :desc), do: unwrap_sort_by_desc(list, [])
defp unwrap_sort_by_asc([]), do: []
defp unwrap_sort_by_asc([{_val, elem} | tail]), do: [elem | unwrap_sort_by_asc(tail)]
defp unwrap_sort_by_desc([], acc), do: acc
defp unwrap_sort_by_desc([{_val, elem} | tail], acc),
do: unwrap_sort_by_desc(tail, [elem | acc])
@doc """
Safe equivalent of `Enum.max_by/3`, returning the element of a non-empty
enumerable for which `fun` gives the maximum comparable value.
## Examples
iex> Cmp.max_by([%{x: 1}, %{x: 3}, %{x: 2}], & &1.x)
%{x: 3}
Respects semantic comparison:
iex> Cmp.max_by([%{date: ~D[2020-03-02]}, %{date: ~D[2019-06-06]}], & &1.date)
%{date: ~D[2020-03-02]}
Raises a `Cmp.TypeError` on non-uniform enumerables:
iex> Cmp.max_by([%{x: 1}, %{x: nil}, %{x: 2}], & &1.x)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Raises an `Enum.EmptyError` on empty enumerables:
iex> Cmp.max_by([], & &1.x)
** (Enum.EmptyError) empty error
"""
@spec max_by(Enumerable.t(elem), (elem -> Comparable.t())) :: elem when elem: term()
def max_by(enumerable, fun)
def max_by([head | tail], fun) when is_function(fun, 1) do
case fun.(head) do
val when is_number(val) ->
max_by_list_numbers(tail, head, val, fun)
val when is_binary(val) ->
max_by_list_binaries(tail, head, val, fun)
val ->
module = Comparable.impl_for!(val)
max_by_list(tail, head, val, fun, module)
end
end
def max_by(enumerable, fun) when is_function(fun, 1) do
Enum.max_by(enumerable, fun, __MODULE__)
end
@compile {:inline, max_by_list: 5, max_by_list_numbers: 4, max_by_list_binaries: 4}
defp max_by_list([], elem, _val, _fun, _module), do: elem
defp max_by_list([head | tail], elem, val, fun, module) do
new_val = fun.(head)
case module.compare(val, new_val) do
:lt -> max_by_list(tail, head, new_val, fun, module)
_ -> max_by_list(tail, elem, val, fun, module)
end
end
for {fun, guard} <- [
max_by_list_numbers: :is_number,
max_by_list_binaries: :is_binary
] do
defp unquote(fun)([], elem, _val, _fun), do: elem
defp unquote(fun)([head | tail], elem, val, fun) do
case fun.(head) do
other when not unquote(guard)(other) -> raise Cmp.TypeError, left: val, right: other
new_val when new_val > val -> unquote(fun)(tail, head, new_val, fun)
_ -> unquote(fun)(tail, elem, val, fun)
end
end
end
@doc """
Safe equivalent of `Enum.min_by/3`, returning the element of a non-empty
enumerable for which `fun` gives the minimum comparable value.
## Examples
iex> Cmp.min_by([%{x: 1}, %{x: 3}, %{x: 2}], & &1.x)
%{x: 1}
Respects semantic comparison:
iex> Cmp.min_by([%{date: ~D[2020-03-02]}, %{date: ~D[2019-06-06]}], & &1.date)
%{date: ~D[2019-06-06]}
Raises a `Cmp.TypeError` on non-uniform enumerables:
iex> Cmp.min_by([%{x: 1}, %{x: nil}, %{x: 2}], & &1.x)
** (Cmp.TypeError) Failed to compare incompatible types - left: 1, right: nil
Raises an `Enum.EmptyError` on empty enumerables:
iex> Cmp.min_by([], & &1.x)
** (Enum.EmptyError) empty error
"""
@spec min_by(Enumerable.t(elem), (elem -> Comparable.t())) :: elem when elem: term()
def min_by(enumerable, fun)
def min_by([head | tail], fun) when is_function(fun, 1) do
case fun.(head) do
val when is_number(val) ->
min_by_list_numbers(tail, head, val, fun)
val when is_binary(val) ->
min_by_list_binaries(tail, head, val, fun)
val ->
module = Comparable.impl_for!(val)
min_by_list(tail, head, val, fun, module)
end
end
def min_by(enumerable, fun) when is_function(fun, 1) do
Enum.min_by(enumerable, fun, __MODULE__)
end
@compile {:inline, min_by_list: 5, min_by_list_numbers: 4, min_by_list_binaries: 4}
defp min_by_list([], elem, _val, _fun, _module), do: elem
defp min_by_list([head | tail], elem, val, fun, module) do
new_val = fun.(head)
case module.compare(val, new_val) do
:gt -> min_by_list(tail, head, new_val, fun, module)
_ -> min_by_list(tail, elem, val, fun, module)
end
end
for {fun, guard} <- [
min_by_list_numbers: :is_number,
min_by_list_binaries: :is_binary
] do
defp unquote(fun)([], elem, _val, _fun), do: elem
defp unquote(fun)([head | tail], elem, val, fun) do
case fun.(head) do
other when not unquote(guard)(other) -> raise Cmp.TypeError, left: val, right: other
new_val when new_val < val -> unquote(fun)(tail, head, new_val, fun)
_ -> unquote(fun)(tail, elem, val, fun)
end
end
end
end