A few notes. :-)
At:
https://github.com/Qqwy/elixir_experimental_comparable/blob/master/lib/comparable.ex#L27-L37
Shouldn't this:
```elixir
defcomparison(Integer, RomanNumeral) do
def compare(int, %RomanNumeral{num: num}) when num < int, do: :<
def compare(int, %RomanNumeral{num: num}) when num > int, do: :>
def compare(int, %RomanNumeral{}) , do: :=
end
```
Actually be something like:
```elixir
defcomparison(Integer, RomanNumeral) do
def compare(int, %RomanNumeral{num: num}), do: Comparable.compare(int, num)
end
```
Even for primitive types it can be a bad idea not to delegate the compare
function (the compiler can optimize it or so, especially with macros).
What if someone stuff something that is not an integer in the RomanNumeral
somehow, they would then report as equal even when not. That brings up the
thing, maybe it should be defined as:
```elixir
defcomparison(Integer, %RomanNumeral{num: Integer}) do
def compare(int, %RomanNumeral{num: num}), do: Comparable.compare(int, num)
end
```
The defcomparison line itself changed to be explicit as to what is accepted
(which can be performed via a match/when internally). Instead of having
automatic naming for things like Integer, you could also do this:
```elixir
defcomparison(int, %RomanNumeral{num: num}) when is_int(int) and
is_int(num) do
def compare(int, %RomanNumeral{num: num}), do: Comparable.compare(int, num)
end
```
And considering that, maybe instead just entirely shrink it down to:
```elixir
defcomparison(int, %RomanNumeral{num: num}) when is_int(int) and
is_int(num) do
Comparable.compare(int, num)
end
```
I.E., make the `defcomparison` call itself define the function, of which
you could put multiple:
```elixir
defcomparison(int, %RomanNumeral{num: num}) when is_int(int) and
is_int(num) do
Comparable.compare(int, num)
end defcomparison(float, %RomanNumeral{num: num}) when is_float(float) and
is_int(num) do Comparable.compare(float, num) end
```
Although in this simple case you could simplify it down to just:
```elixir
defcomparison(v, %RomanNumeral{num: num}) when is_int(num) do
Comparable.compare(v, num)
end
```
This states that %RomanNumerals must always hold an integer, but you can
compare anything to the integer that you could normally compare to an
integer (whether another integer, float, some custom type, etc...).
But the above latest syntax simplifies it, you could put multiple
defcomparisons in a row like normal function heads (the 'use Comparable'
declaration in the module can do the combining and optimizing work), you
use normal Elixir syntax to deconstruct the types and put on when
conditions, *and* you could potentially restrict the syntax to not allow
costly calls so you could use compares in `when`'s, perhaps something as
simple as like:
```elixir
defcomparison(v, %RomanNumeral{num: num}) when is_int(num) and
Comparable.compare(v, num)
```
That would restrict the ability to do costly computations (like the loading
a file mentioned in the below post), but does allow `when` usage. Honestly
though the one just above that is probably what I'd go with, it allows
complex usage (like loading a file), if someone wants to match in heads
they can always call it then pass to another defp head matchers or a case
expression anyway.
Also, https://github.com/expede/type_class is always an interesting idea to
bake in to Elixir, and could handle this case too (an `Elixir.Ord`
typeclass or so? plus the optional fuzzy testing at compile-time to ensure
the typeclass is correct for the implemented types). ^.^
/me just had coffee so may not be awake enough for this to make sense yet.
On Tuesday, November 22, 2016 at 6:27:34 AM UTC-7, Wiebe-Marten Wijnja
wrote:
>
> *1. Should it only compare structs of the same kind:*
>
> I believe that when only comparisons between the same kind of data type
> (structs or built-in) are needed, a MyDatatype.compare function would be
> enough, which would mean that the core language does not need to be changed.
>
> However:
> *2. Or should it compare across types.*
> I think that this is much more useful. Some examples:
> - Comparisons between different kinds of dates and times, as is already
> the case right now.
> - Comparisons between different kinds of numeric data types (built-in
> integers, built-in floats, decimals, rationals, etc)
> - Comparisons between different kinds of data types representing parts of
> a hierarchy. (An example: Chain of command)
>
> Of course, it is possible 'fake' some of these behaviours by wrapping the
> different types in a single struct, and define a single-type comparison for
> that one. (For instance: Instead of a General, Colonel and Sergeant struct,
> have a single 'Soldier' struct that contains details about rank in another
> way.) But this means that when comparing two data types, both first need to
> be wrapped, so I think this is not desirable.
>
>
> *how would the protocol dispatch work. Should we sort both elements and
> dispatch the first as first?*
> A comparison (possibly across types) is an operation working on an ordered
> pair of datatypes.
>
> To make the dispatching underwater work, we could do one of the following:
> I see two possibilities:
> a) At runtime, sort the element module names (for built-in types, these
> are the 'fake' module names Integer, Float, etc. that are also used to
> define `defprotocol` and friends underwater) and dispatch on the first.
> b) At compiletime, look at what way around the comparison is defined, and
> automatically define the other way around as its converse.
>
> Advantage of a) is smaller code size, at the drawback of slower execution.
> b) has twice the code size, but is probably faster.
>
> For the user defining the custom comparison implementation this does not
> really matter.
> What does matter, is that for the end user this is a 'protocol
> implementation' working with *two* datatypes instead of one. This means
> that, in one way or other, a way to specify this kind of 'protocol'
> implementation needs to be possible. Probably the easiest way (which is
> included in the example implementation
> <https://github.com/Qqwy/elixir_experimental_comparable> that I
> constructed together with @OpenmindDL1) would be to add a macro like
> `defcomparable FirstType SecondType do ... end` where there _has_ to be an
> implementation (using one or multiple clauses) of a function named
> 'compare'.
> There might be other ways to do this. If someone has another idea, please
> do tell.
> One possibility might be something akin to Clojure's multimethod approach
> (basically generic 'Protocol' dispatching but possibly on other or multiple
> arguments instead of only the first) but this introduces a whole layer of
> additional complexity, which I think should best be avoided in the spirit
> of KISS.
>
> *Note that Date.compare, Time.compare, and friends changed on Elixir
> master and are now able to compare across types. It does a field-based
> comparison instead of a struct-based comparison. For example,
> Date.compare/2 can compare any type that contains the fields year, month
> and day.*
>
> I just looked at the source of this because I was curious how it was
> implemented. Maybe it is important to note that for Date, it is also
> required that the `calendar` key is set to match `Calendar.Iso`, and 'any
> type' means 'any custom type built on top of a map'.
> It is great that these comparison functions have been added. What it does
> not solve (and that is something this proposal might change) is what should
> happen when the second date is from a different calendar.
>
> The field-based lookup is definitely better than a structname-based
> lookup, but this will only work if the different kind of data structures
> happen to use the same names for the same values. This is very similar to
> 'duck typing' in a way. For instance, I could make a %Duration{hours:,
> minutes: seconds:} struct. It would be possible to use
> `Time.compare(myDuration)` but of course this is a nonsensical calculation.
>
> Another problem would be that the system breaks down if we have a cyclic
> relationship: Take Rock, Paper, Scissors. Regardless of if they are part of
> the same type or different types, we want Rock to beat (be greater than)
> Scissors, Paper to beat Rock, and Scissors to beat Paper. If the only
> possibility would be to specify some fields that are transformed into a
> tuple which is then compared using Erlang's built-in term ordering, we are
> stuck.
> Or what if we want to compare two things that can only be compared once a
> complicated calculation is performed using the data that is stored inside
> the struct? A struct might contain only a filename, but represent the file
> itself.
>
> I think a better approach would be to allow the specification of a
> comparison implementation as one or multiple function clauses of the
> `compare` function.
> This indeed has the implication that it will be impossible to use
> `compare` in guards (even if map-field access becomes possible), but I
> believe that this freedom is necessary, because of the restrictions
> outlined above.
>
>
>
> On Tuesday, November 22, 2016 at 9:28:51 AM UTC+1, José Valim wrote:
>>
>> We should continue the discussion here and include the implementation you
>> have so far. I believe implementation wise, the two main questions were:
>>
>> 1. Should it only compare structs of the same kind
>>
>> 2. Or should it compare across types. If comparing across types, how
>> would the protocol dispatch work. Should we sort both elements and dispatch
>> the first as first?
>>
>> Note that Date.compare, Time.compare, and friends changed on Elixir
>> master and are now able to compare across types. It does a field-based
>> comparison instead of a struct-based comparison. For example,
>> Date.compare/2 can compare any type that contains the fields year, month
>> and day.
>>
>> When it comes to adding it to Elixir, the main drawback was that we
>> didn't agree it added enough to the language to warrant its inclusion in
>> Elixir. In particular, if the feature feels detached from Elixir overall,
>> i.e. it is just a new function+protocol that doesn't actually integrate
>> with Elixir features, such as guards, then it is probably best to leave it
>> as a library. Especially because it may affect future decisions in the
>> language.
>>
>> For example, imagine we can access map fields or write case expressions
>> in guards. Would that allow us to write a guard-able compare expression?
>> How that would affect the compare implementation?
>>
>> Those are the same reasons why we decided to not include Decimal as part
>> of the standard library, it's why we don't provide an Array/Vector/Queue,
>> etc. If it is limited in scope it is probably best to leave it apart so we
>> still have the option to provide something truly integrated later on.
>>
>>
>> *José Valim*
>> www.plataformatec.com.br
>> Skype: jv.ptec
>> Founder and Director of R&D
>>
>>
--
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/b66b39b7-521a-4646-a86d-bed4eb103745%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.