View Source Cmp (Cmp v0.1.2)

Semantic comparison and sorting for Elixir.

why-cmp

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

1. Structural comparisons

Built-ins use structural comparison over semantic 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

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

What's in the box

supported-types

Supported types

The Cmp.Comparable protocol is implemented for the following types:

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

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)

limitations

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

Link to this section Summary

Functions

Returns :gt if left is semantically greater than right, lt if left is less than right, and :eq if they are equal.

Safe equivalent to ==/2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent to >/2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent to >=/2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent to </2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent to <=/2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent of Enum.max/2, returning the minimum of a non-empty enumerable of comparables.

Safe equivalent to max/2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent of Enum.max_by/3, returning the element of a non-empty enumerable for which fun gives the maximum comparable value.

Safe equivalent of Enum.min/2, returning the maximum of a non-empty enumerable of comparables.

Safe equivalent to min/2, which only works if both types are compatible and uses semantic comparison.

Safe equivalent of Enum.min_by/3, returning the element of a non-empty enumerable for which fun gives the minimum comparable value.

Link to this section Functions

@spec compare(Cmp.Comparable.t(), Cmp.Comparable.t()) :: :eq | :lt | :gt

Returns :gt if left is semantically greater than right, lt if left is less than right, and :eq if they are equal.

examples

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: ""

Safe equivalent to ==/2, which only works if both types are compatible and uses semantic comparison.

examples

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: ""

Safe equivalent to >/2, which only works if both types are compatible and uses semantic comparison.

examples

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:

iex> Cmp.gt?(~D[2020-03-02], ~D[2019-06-06])
true

~D[2020-03-02] > ~D[2019-06-06]
false
@spec gte?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()

Safe equivalent to >=/2, which only works if both types are compatible and uses semantic comparison.

examples

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:

iex> Cmp.gte?(~D[2020-03-02], ~D[2019-06-06])
true

~D[2020-03-02] >= ~D[2019-06-06]
false

Safe equivalent to </2, which only works if both types are compatible and uses semantic comparison.

examples

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:

iex> Cmp.lt?(~D[2019-06-06], ~D[2020-03-02])
true

~D[2019-06-06] < ~D[2020-03-02]
false
@spec lte?(Cmp.Comparable.t(), Cmp.Comparable.t()) :: boolean()

Safe equivalent to <=/2, which only works if both types are compatible and uses semantic comparison.

examples

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:

iex> Cmp.lte?(~D[2019-06-06], ~D[2020-03-02])
true

~D[2019-06-06] <= ~D[2020-03-02]
false
@spec max(Enumerable.t(elem)) :: elem when elem: Cmp.Comparable.t()

Safe equivalent of Enum.max/2, returning the minimum of a non-empty enumerable of comparables.

examples

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(value, value) :: value when value: Cmp.Comparable.t()

Safe equivalent to max/2, which only works if both types are compatible and uses semantic comparison.

examples

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:

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_by(Enumerable.t(elem), (elem -> Cmp.Comparable.t())) :: elem
when elem: term()

Safe equivalent of Enum.max_by/3, returning the element of a non-empty enumerable for which fun gives the maximum comparable value.

examples

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 min(Enumerable.t(elem)) :: elem when elem: Cmp.Comparable.t()

Safe equivalent of Enum.min/2, returning the maximum of a non-empty enumerable of comparables.

examples

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(value, value) :: value when value: Cmp.Comparable.t()

Safe equivalent to min/2, which only works if both types are compatible and uses semantic comparison.

examples

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:

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_by(Enumerable.t(elem), (elem -> Cmp.Comparable.t())) :: elem
when elem: term()

Safe equivalent of Enum.min_by/3, returning the element of a non-empty enumerable for which fun gives the minimum comparable value.

examples

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
Link to this function

sort(enumerable, order \\ :asc)

View Source
@spec sort(Enumerable.t(elem), :asc | :desc) :: Enumerable.t(elem)
when elem: Cmp.Comparable.t()

Safe equivalent of Enum.sort/2.

examples

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
Link to this function

sort_by(enumerable, fun, order \\ :asc)

View Source
@spec sort_by(Enumerable.t(elem), (elem -> Cmp.Comparable.t()), :asc | :desc) ::
  Enumerable.t(elem)
when elem: term()

Safe equivalent of Enum.sort_by/2.

examples

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"