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.

Reply via email to