Thank you for your reply, Michał. You are right, it does not. I don't know why I was thinking that Erlang Term Ordering would be important for Enum.sort.
This means that it probably is best to use `:lt`, `:gt`, `:eq` as outcome, since that is what Elixir itself already uses. For proper use with Enum.sort, we could, besides `Kernel.compare/2` create `Kernel.lt?`,`Kernel.lte?`, `Kernel.equal?``Kernel.gte?`, `Kernel.gt?`, although this becomes such a large number of functions that we might put it under their own namespace (`Compare`?) instead. We could change `Enum.sort` to use `&Kernel.lte?/2` by default, rather than `&<=/2`. Although this might have slight efficiency implications for existing applications, I think it would uphold the Principle of Least Surprise better. On Friday, June 9, 2017 at 1:34:47 PM UTC+2, Michał Muskała wrote: > > Just a quick remark (for anything more this requires a deeper analysis): > > Why would erlang term ordering be important for the return value of > compare/2 function? The function you pass to Enum.sort should return true > or false, so you can't use it there directly anyway. > > Michał. > > On 9 Jun 2017, 13:21 +0200, Wiebe-Marten Wijnja <[email protected] > <javascript:>>, wrote: > > 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] <javascript:>. > To view this discussion on the web visit > https://groups.google.com/d/msgid/elixir-lang-core/c55bd3c0-a2a9-4955-a62f-9184a587a029%40googlegroups.com > > <https://groups.google.com/d/msgid/elixir-lang-core/c55bd3c0-a2a9-4955-a62f-9184a587a029%40googlegroups.com?utm_medium=email&utm_source=footer> > . > For more options, visit https://groups.google.com/d/optout. > > -- 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/ae813abb-05a1-4f01-983d-fa76bf760bf1%40googlegroups.com. For more options, visit https://groups.google.com/d/optout.
