I would like to revive this topic, because I am still looking for a good
way to perform comparisons on custom structures.
Right now there is no standardized way to do this, resulting in widely
varying APIs❧, many of which are incompatible with e.g. `Enum.sort` because
of the chosen output format, which would be one of the most obvious and
frequent use-cases of comparison operations.
So, I would like to propose the following:
- Add a `Comparable` protocol (Or behaviour?), containing
`Comparable.compare(MyStruct.t, MyStruct.t)`.
- Choose a sensible output format for this compare function. This is
something we should discuss:
- In existing parts of Elixir Core (the (Date)(Time) modules), `:lt`,
`:eq`, `:gt` are used. But these do not themselves follow the Erlang Term
Ordering, and therefore cannot be used directly for things
like`Enum.sort(enumerable, &Kernel.compare/2)`.
- Many other languages, and multiple existing libraries return an
integer `-1, 0, 1`. This does follow the Erlang Term Ordering. Main
disadvantage: They are not very descriptive, someone might think that any
integer could be returned.
- `:<`, `:=`, `:>` are another possibility: Because of the ASCII
ordering, these follow the Erlang Term Ordering, and are arguably more
descriptive than the integers. Main disadvantage: They are not widely
used
yet.
- What to do on error? Raise? Or return `nil`? `{:error,
some_reason}` is another possibility (Although we do not use an `{:ok,
success}` here, so maybe this is confusing?).
- Add `Kernel.compare/2`, which is overridden to use simple comparison
for built-in datatypes, and dispatches to the protocol for structs (raising
when the structs are not of the same type).
Aside from this, (this can be accepted/rejected independently!) I see value
in a `Comparable.coerce(builtin)`, which can be optionally implemented to
allow a builtin datatype to be converted to the struct of the other, before
comparing.
❧ *About widely-varying APIs:*
- Time.compare, Date.compare, DateTime.compare return `:lt`, `:eq` or `:gt`
- Timex.compare returns integer (-1, 0, 1) or {:error, reason} and has an
optional granularity.
- Decimal.compare returns #Decimal<-1>, #Decimal<0>, #Decimal<1> or
#Decimal<NaN>.
- Decimal.cmp returns `:lt`, :eq` or `:gt` and raises on NaN.
- Ratio.compare returns integer (-1, 0, 1) or raises Ratio.ComparisonError.
On Wednesday, December 14, 2016 at 12:49:26 PM UTC+1, Wiebe-Marten Wijnja
wrote:
>
> I have been thinking longer about this.
>
> I think that the across-type implementation is overkill; I have a lot of
> trouble to come up with cases where this would be useful: Cases that I can
> think of can readily be solved by wrapping the inner structure in a
> containing struct, which is obviously more clear/explicit than providing a
> two-typed frankenprotocol.
>
>
> I have been working on Numbers, which is basically dispatches arithmetic
> operations to any structs that implements its standardized Numeric
> behaviour. In this case, this means that functions/modules/structs can be
> written that wrap *any* kind of thing that implements the behaviour,
> which means that e.g. my Tensor library allows addition/multiplication to
> performed regardless of if the contents of the vectors/matrices/tensors are
> `Integer`s, `Float`s, `Decimal`s, `Ratio`nals or even `ComplexNum`bers.
>
> I think that such a *standardized* way constructing something still is
> very important in the core language, because a standardized API means that
> modules consuming the API can use any modules(/data types) that are
> exposing the API.
>
> I now envision the following, much simpler and less 'new language
> feature'-heavy than my original proposal:
>
> ---------------------
>
> There is a *normal* protocol called Comparable, exposing two functions
> that can be overridden:
>
> - *compare(a, b)* compares the two structs `a` and `b` of the same
> type. This function should return `:lt`, `:gt`, or `:eq` (keeping with the
> specification that DateTime.compare and Time.compare already follow). If
> there is no sensible way to compare the two types, a
> *Comparable.CannotCompareError* should be raised, with a describing
> error message. The *Any* implementation always raises this error.
> - *coerce(some_builtin_value)*. Can optionally(!) be implemented to
> allow certain standard data types (e.g. numbers, or strings) to be
> automatically converted to the type of the thing we want to compare it
> with, to allow shorter notation for things like `compare(Decimal.new(2),
> 3)`. The *Any* implementation always raises
> *Comparable.CannotCompareError*.
>
> There is a new function in the Kernel function *Kernel.compare(a, b)* has
> the following variants:
>
> # struct <=> struct
> Kernel.compare(a = %someStruct{}, b = %someStruct{}), do: Comparable.
> compare(a, b)
> # struct <=> differentStruct
> Kernel.compare(a = %someStruct{}, b = %someDifferentStruct{}), do: raise
> Comparable.CannotCompareError, message: "Cannot compare #{inspect(a)}
> with #{inspect(b)}."
> # struct <=> builtin
> Kernel.compare(a = %someStruct{}, b), do: Comparable.compare(a, Comparable
> .coerce(b))
> # builtin <=> struct
> Kernel.compare(a, b = %someStruct{}), do: Comparable.compare(Comparable.
> coerce(a), b)
> # builtin <=> builtin, use Erlang's built-in term ordering.
> Kernel.compare(a, b) when a == b, do: :eq
> Kernel.compare(a, b) when a < b, do: :lt
> Kernel.compare(a, b), do: :gt
>
>
>
>
>
>
>
>
>
--
You received this message because you are subscribed to the Google Groups
"elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/elixir-lang-core/c55bd3c0-a2a9-4955-a62f-9184a587a029%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.