A More Human DateTime Comparison API

390 views
Skip to first unread message

Cliff

unread,
Oct 29, 2022, 4:51:48 PM10/29/22
to elixir-lang-core
I always struggle to read code that compares `DateTime`s using DateTime.compare/2, so I've been playing with a more readable API for it. I've come up with this, that feels pretty nice to use:

```
defmodule DateTime do
  def is?(a, [before: b]) do
    :lt == DateTime.compare(a, b)
  end

  def is?(a, [after: b]) do
    :gt == DateTime.compare(a, b)
  end
end
```

Sample:
```
t1 = DateTime.utc_now()
t2 = DateTime.add(t1, 1, :second)

true = DateTime.is?(t1, before: t2)
false = t1
             |> DateTime.is?(after: t2)
```

From some discussion in the Discord, it seems I'm not the only one who struggles with the :gt/:eq/:lt and argument order in DateTime.compare. It's also been pointed out that this doesn't follow elixir's stdlib api conventions, so maybe two separate `DateTime.is_before(a, b)` and `DateTime.is_after(a, b)` functions would be a better fit.

Thoughts?

José Valim

unread,
Oct 30, 2022, 3:15:14 AM10/30/22
to elixir-l...@googlegroups.com
I am definitely in favor of clearer APIs.

However, it would probably be best to explore how different libraries in different languages tackle this. Can you please explore this? In particular, I am curious to know if before/after mean "<" and ">" respectively or if they mean "<=" and "=>" (I assume the former). And also if some libraries feel compelled to expose functions such as "after_or_equal" or if users would have to write Date.equal?(date1, date2) or Date.earlier?(date1, date2), which would end-up doing the double of conversions.

Kevin Johnson

unread,
Oct 30, 2022, 7:56:49 AM10/30/22
to elixir-l...@googlegroups.com
What about making the atoms a bit clearer? e.g. `:a_lt_b` and `:a_gt_b` to replace `:lt` and `:gt`? You can always introduce a third optional flag to: `DateTime.compare/2` to maintain backwards compatibility: `DateTime.compare(a, b, verbose: false)`

On Sun, Oct 30, 2022 at 3:15 AM José Valim <jose....@dashbit.co> wrote:
I am definitely in favor of clearer APIs.

However, it would probably be best to explore how different libraries in different languages tackle this. Can you please explore this? In particular, I am curious to know if before/after mean "<" and ">" respectively or if they mean "<=" and "=>" (I assume the former). And also if some libraries feel compelled to expose functions such as "after_or_equal" or if users would have to write Date.equal?(date1, date2) or Date.earlier?(date1, date2), which would end-up doing the double of conversions.

--
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 elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2B3oDVk5hpF7EpfZt4Xa0k7v1Rk%2BxFF9eGtWCXmO4JfpA%40mail.gmail.com.
Message has been deleted

Cliff

unread,
Oct 30, 2022, 1:46:52 PM10/30/22
to elixir-lang-core
I did a bit of research. Many other languages use some form of operator overloading to do datetime comparison. The ones that do something different:
  • Java has LocalDateTime.compareTo(other), returning an integer representing gt/lt/eq. There is also LocalDateTime.isBefore(other), LocalDateTime.isAfter(other), and LocalDateTime.isEqual(other). The LocalDateTime.is{Before, After} methods are non-inclusive (<, >) comparisons. They are instance methods, so usage is like `myTime1.isBefore(myTime2)`
  • OCaml's "calendar" library provides a Date.compare function that returns an integer representing gt/lt/eq (for use in OCaml's List.sort function, which sorts a list according to the provided comparison function). It also provides Date.>, and Date.>=, etc. Worth noting is that OCaml allows you to do expression-level module imports, like Date.(my_t1 > my_t2) to use Date's > function in the parenthesized expression without needing to open Date in the entire scope ("open" is OCaml's "import") - this could potentially be possible in Elixir using a macro?
  • Golang: t1.After(t2), t1.Before(t2), t1.Equal(t2). Non-inclusive (> and <).
  • Clojure clj-time library: (after? t1 t2), (before? t1 t2), and (equal? t1 t2). IMO the argument order is still confusing in these.

José Valim

unread,
Oct 30, 2022, 5:26:42 PM10/30/22
to elixir-l...@googlegroups.com
Thank you!

A PR that adds before?/after? to Time, Date, NaiveDateTime, and DateTime is welcome!

--
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 elixir-lang-co...@googlegroups.com.

Simon McConnell

unread,
Oct 30, 2022, 9:16:10 PM10/30/22
to elixir-lang-core
would we want on_or_after? and on_or_before? as well then?  Or something like DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

José Valim

unread,
Oct 31, 2022, 2:23:16 AM10/31/22
to elixir-l...@googlegroups.com
My thought process is that a simple to use API should be the focus, because we already have a complete API in Date.compare/2 and friends.

Jon Rowe

unread,
Oct 31, 2022, 3:02:51 AM10/31/22
to elixir-l...@googlegroups.com
I'm not sure the name is right, but I like

DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

As a usage api, we could actually have `compare?/3` especially as the name doesn't overlap with `compare/2` which would hopefully alleviate anyones concerns about the return type changing

Zach Daniel

unread,
Oct 31, 2022, 3:08:35 AM10/31/22
to elixir-l...@googlegroups.com
I wonder how much of the issue is the Api and how much of the issue is just the docs? I.e its not a given that all arguments in every position always make sense, but we typically rely on things like elixir_ls to help us when the answer isn't obvious.

Could we perhaps just improve the docs in some way? i.e update the specs to say `datetime :: Calendar.datetime(), compares_to :: Calendar.datetime()`, and have the args say `compare(datetime, compares_to)` and have part of the first line of text say something a bit more informative?


On Mon, Oct 31, 2022 at 3:02 AM, Jon Rowe <ma...@jonrowe.co.uk> wrote:
I'm not sure the name is right, but I like

DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

As a usage api, we could actually have `compare?/3` especially as the name doesn't overlap with `compare/2` which would hopefully alleviate anyones concerns about the return type changing

On Mon, 31 Oct 2022, at 6:23 AM, José Valim wrote:
My thought process is that a simple to use API should be the focus, because we already have a complete API in Date.compare/2 and friends.
On Mon, Oct 31, 2022 at 02:16 Simon McConnell <simonmcconnell@gmail.com> wrote:
would we want on_or_after? and on_or_before? as well then?  Or something like DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

On Monday, 31 October 2022 at 7:26:42 am UTC+10 José Valim wrote:
Thank you!

A PR that adds before?/after? to Time, Date, NaiveDateTime, and DateTime is welcome!


On Sun, Oct 30, 2022 at 6:46 PM Cliff <notcliff...@gmail.com> wrote:
I did a bit of research. Many other languages use some form of operator overloading to do datetime comparison. The ones that do something different:
  • Java has LocalDateTime.compareTo(other), returning an integer representing gt/lt/eq. There is also LocalDateTime.isBefore(other), LocalDateTime.isAfter(other), and LocalDateTime.isEqual(other). The LocalDateTime.is{Before, After} methods are non-inclusive (<, >) comparisons. They are instance methods, so usage is like `myTime1.isBefore(myTime2)`
  • OCaml's "calendar" library provides a Date.compare function that returns an integer representing gt/lt/eq (for use in OCaml's List.sort function, which sorts a list according to the provided comparison function). It also provides Date.>, and Date.>=, etc. Worth noting is that OCaml allows you to do expression-level module imports, like Date.(my_t1 > my_t2) to use Date's > function in the parenthesized expression without needing to open Date in the entire scope ("open" is OCaml's "import") - this could potentially be possible in Elixir using a macro?
  • Golang: t1.After(t2), t1.Before(t2), t1.Equal(t2). Non-inclusive (> and <).
  • Clojure clj-time library: (after? t1 t2), (before? t1 t2), and (equal? t1 t2). IMO the argument order is still confusing in these.



On Sunday, October 30, 2022 at 3:15:14 AM UTC-4 José Valim wrote:
I am definitely in favor of clearer APIs.

However, it would probably be best to explore how different libraries in different languages tackle this. Can you please explore this? In particular, I am curious to know if before/after mean "<" and ">" respectively or if they mean "<=" and "=>" (I assume the former). And also if some libraries feel compelled to expose functions such as "after_or_equal" or if users would have to write Date.equal?(date1, date2) or Date.earlier?(date1, date2), which would end-up doing the double of conversions.


--
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 elixir-lang-co...@googlegroups.com.


--
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 elixir-lang-core+unsubscribe@googlegroups.com.


--
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 elixir-lang-core+unsubscribe@googlegroups.com.

--
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 elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/2e821e87-6ee0-4702-b69f-e2616b61b1dd%40app.fastmail.com.

Simon McConnell

unread,
Oct 31, 2022, 3:45:15 AM10/31/22
to elixir-lang-core
DateTime.before?(a, b) is much nicer than DateTime.compare(a, b) == :lt.  It doesn't completely remove the argument order issue but I reckon it would resolve it for me.  I run DateTime.compare(a, b) in iex every time I use the function because I'm terribly forgetful and paranoid.  I would prefer DateTime.eq?/lt?/le?/gt?/ge? instead of before?/after?/on_or_before?/on_or_after? which is shorter, matches compare/2 and might allow the le/ge equivalents to sneak through.  I think it would be a shame to leave out le and ge.

DateTime.is?/compare?(a, :lt, b) is a whole lot less ambiguous to me.  It reads how you would write it in maths or spoken language.

On Monday, 31 October 2022 at 5:08:35 pm UTC+10 zachary....@gmail.com wrote:
I wonder how much of the issue is the Api and how much of the issue is just the docs? I.e its not a given that all arguments in every position always make sense, but we typically rely on things like elixir_ls to help us when the answer isn't obvious.

Could we perhaps just improve the docs in some way? i.e update the specs to say `datetime :: Calendar.datetime(), compares_to :: Calendar.datetime()`, and have the args say `compare(datetime, compares_to)` and have part of the first line of text say something a bit more informative?


On Mon, Oct 31, 2022 at 3:02 AM, Jon Rowe <ma...@jonrowe.co.uk> wrote:
I'm not sure the name is right, but I like

DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

As a usage api, we could actually have `compare?/3` especially as the name doesn't overlap with `compare/2` which would hopefully alleviate anyones concerns about the return type changing

On Mon, 31 Oct 2022, at 6:23 AM, José Valim wrote:
My thought process is that a simple to use API should be the focus, because we already have a complete API in Date.compare/2 and friends.
On Mon, Oct 31, 2022 at 02:16 Simon McConnell <simonmc...@gmail.com> wrote:
would we want on_or_after? and on_or_before? as well then?  Or something like DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

On Monday, 31 October 2022 at 7:26:42 am UTC+10 José Valim wrote:
Thank you!

A PR that adds before?/after? to Time, Date, NaiveDateTime, and DateTime is welcome!


On Sun, Oct 30, 2022 at 6:46 PM Cliff <notcliff...@gmail.com> wrote:
I did a bit of research. Many other languages use some form of operator overloading to do datetime comparison. The ones that do something different:
  • Java has LocalDateTime.compareTo(other), returning an integer representing gt/lt/eq. There is also LocalDateTime.isBefore(other), LocalDateTime.isAfter(other), and LocalDateTime.isEqual(other). The LocalDateTime.is{Before, After} methods are non-inclusive (<, >) comparisons. They are instance methods, so usage is like `myTime1.isBefore(myTime2)`
  • OCaml's "calendar" library provides a Date.compare function that returns an integer representing gt/lt/eq (for use in OCaml's List.sort function, which sorts a list according to the provided comparison function). It also provides Date.>, and Date.>=, etc. Worth noting is that OCaml allows you to do expression-level module imports, like Date.(my_t1 > my_t2) to use Date's > function in the parenthesized expression without needing to open Date in the entire scope ("open" is OCaml's "import") - this could potentially be possible in Elixir using a macro?
  • Golang: t1.After(t2), t1.Before(t2), t1.Equal(t2). Non-inclusive (> and <).
  • Clojure clj-time library: (after? t1 t2), (before? t1 t2), and (equal? t1 t2). IMO the argument order is still confusing in these.



On Sunday, October 30, 2022 at 3:15:14 AM UTC-4 José Valim wrote:
I am definitely in favor of clearer APIs.

However, it would probably be best to explore how different libraries in different languages tackle this. Can you please explore this? In particular, I am curious to know if before/after mean "<" and ">" respectively or if they mean "<=" and "=>" (I assume the former). And also if some libraries feel compelled to expose functions such as "after_or_equal" or if users would have to write Date.equal?(date1, date2) or Date.earlier?(date1, date2), which would end-up doing the double of conversions.


--
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 elixir-lang-co...@googlegroups.com.


--
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 elixir-lang-co...@googlegroups.com.


--
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 elixir-lang-co...@googlegroups.com.

--
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 elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/2e821e87-6ee0-4702-b69f-e2616b61b1dd%40app.fastmail.com.

Cliff

unread,
Oct 31, 2022, 12:10:09 PM10/31/22
to elixir-lang-core
I prefer the form DateTime.is(a, operator, b), but I agree with others that it would need a more sensible name than "is".

Regarding the form DateTime.before?(a, b), I could still see myself getting confused by argument order. before?(a, b) might be read as "before A happened, B happened", rather than the intended "A happened before B". the is(a, :before, b) form, however, is read exactly how it would be spoken.

Regarding comparison inclusivity, another possibility is a keyword option: DateTime.before?(a, b, inclusive: true)

and...@dryga.com

unread,
Oct 31, 2022, 12:23:54 PM10/31/22
to elixir-lang-core
Hey guys, as an idea why don't we reuse atoms from Ecto: 
  • :less_than
  • :greater_than
  • :less_than_or_equal_to
  • :greater_than_or_equal_to
  • :equal_to
  • :not_equal_to
I feel like they are fairly common nowadays and even though it's more to type make it easier to understand when you want an inclusive comparison. 

We can later make it part of all modules that have `compare/2` (Date, DateTime, Time, Version, etc).

Cliff

unread,
Oct 31, 2022, 12:32:19 PM10/31/22
to elixir-lang-core
I could also see myself using a |> DateTime.before?(b)

Luiz Damim

unread,
Oct 31, 2022, 12:35:05 PM10/31/22
to elixir-lang-core
I also prefer something like DateTime.compare(a, operator, b).

Operators don't need to be cryptic like :eq, :gt, :lte, etc., we can use the same comparison operators we already are used to:

DateTime.compare(a, :<, b)
DateTime.compare(a, :==, b)
DateTime.compare(a, :>=, b)

It's clear and much less verbose than the Ecto's (which was a great suggestion, by the way).

Cliff

unread,
Oct 31, 2022, 12:44:31 PM10/31/22
to elixir-lang-core
I would prefer the atoms :before, and :after rather than :gt/:greater_than/etc. Since we're already solving the problem of operator/argument ordering, why not remove the final mental barrier of reasoning about whether a time being "greater than" another time means that it is before or after? foo(a, :gt, b) still requires a second thought ("Is a bigger time earlier or later?"), whereas if I read code that said foo(a, :before, b) I would feel confident in my understanding after only the first read.

José Valim

unread,
Oct 31, 2022, 1:16:52 PM10/31/22
to elixir-l...@googlegroups.com
I am not worried about the argument order because in Elixir the subject is always the first argument. So it is always "is date1 before date2?". I like the :inclusive option if the need ever arises.

DateTime.compare(a, :<, b) would get my vote of the alternative proposals but I think it doesn't move much the needle in comparison to DateTime.compare.

Christopher Keele

unread,
Oct 31, 2022, 1:59:12 PM10/31/22
to elixir-lang-core
> DateTime.is?(a, operator, b), when operator :lt | :le | :eq | :ge | :gt, which would capture the :le and :ge options.

> I like the :inclusive option if the need ever arises.

If we combine these proposals, we'd only need the exact options that DateTime.compare already returns (:lt | :eq | :gt).

I also prefer the look of something like :> or :less_than, but I think the consistency's more important in this scenario; DateTime.compare and DateTime.is? should be in sync.

Cliff

unread,
Oct 31, 2022, 2:02:03 PM10/31/22
to elixir-lang-core
> in Elixir the subject is always the first argument

Ah, that clears it up for me, I hadn't yet realized that symmetry in the APIs. I like the before?/after? functions now.

Ben Wilson

unread,
Oct 31, 2022, 2:46:09 PM10/31/22
to elixir-lang-core
> DateTime.compare(a, :<, b) would get my vote of the alternative proposals but I think it doesn't move much the needle in comparison to DateTime.compare.

To me this is a pretty big difference difference, because with an `import` it does 2 things:

1) Eliminates the existence of an irrelevant, boilerplate operator ==
2) positions the 2 values you care about correctly with respect to the relevant operator

When you have

DateTime.compare(a, b) == :lt

it's like RPN, you have to hold a and b in your head, remember their order, then skip past the `==` since it doesn't matter, and finally you get to see your comparison. When discussing this in complex contexts the need to try to distinguish about whether you're talking about what the _function call is equal to_ from whether the values themselves are equal to is actually a pretty big deal. There are basically 4 characters with semantic value, and there rest are boilerplate. When you have a bunch of these all next to each other (like when building up complex range helpers) https://gist.github.com/benwilson512/456735775028c2da5bd38572d25b7813 it's just a ton of data to filter out.

If you could `import DateTime, compare?: 3` this could be

compare?(a, :<, b)
compare?(a, :<=, b)

Cliff

unread,
Oct 31, 2022, 2:54:59 PM10/31/22
to elixir-lang-core
I did some more playing around and created this macro:

defmodule Foo do
  defmacro compare_with(comparison, module) do
    {op, _env, [a, b]} = comparison

    cmp_result = quote do
      unquote(module).compare(unquote(a), unquote(b))
    end

    case op do
      :> ->
        {:==, [], [cmp_result, :gt]}

      :< ->
        {:==, [], [cmp_result, :lt]}

      :>= ->
        {:!=, [], [cmp_result, :lt]}

      :<= ->
        {:!=, [], [cmp_result, :gt]}
    end
  end
end


I don't think it is actually a good solution to this issue, but just wanted to share the idea.

(a >= b) |> compare_with(DateTime)

Boris Kuznetsov

unread,
Oct 31, 2022, 3:42:16 PM10/31/22
to 'Andrey Yugai' via elixir-lang-core
Is it possible to modify language in a way to make >,<, = work for dates?

The datetime's struct has known values which can be pattern matched against and struct comparison, in general, is not used that match, so it shouldn't mess up with already written code (maybe we even fix couple bugs as using >,<,= to compare dates are relatively common first bug for new elixir developers). 

If we can ducktype struct with such attributes and use a regular DateTime.compate/2 to compare it in Kernel.>/2 function and friends.

Ben Wilson

unread,
Oct 31, 2022, 5:14:50 PM10/31/22
to elixir-lang-core
Making < and <= work in general for DateTime has been discussed and isn't feasible. The macro answer I kinda love.

José Valim

unread,
Oct 31, 2022, 5:26:39 PM10/31/22
to elixir-l...@googlegroups.com
Making DateTime.compare?(left, :<=, right) resemble left <= right can be a win but i think it can also cause confusion in that "why not use left <= right in the first place"? And once we import, it makes me wonder why it isn't a protocol so we can compare anything?

I am not saying we shouldn't tackle those problems... but those are likely to take longer discussions.

At the same time, I don't feel we have to pick one option or the other.  So I would start with DateTime.before?/2 and DateTime.after?/2 for now, which is definitely an improvement over the current code and may as well elegantly solve the problem in the long term. If not, it is no problem to restart the discussion.

So a PR for before?/2 and after?/2 (no inclusive for now) on all 4 modules is welcome. :)



Austin Ziegler

unread,
Oct 31, 2022, 6:18:09 PM10/31/22
to elixir-l...@googlegroups.com
I would *personally* appreciate an inclusive option from the start, as sometimes the `b` value is pulled from a database and to make the `before?` work the way `<=` would, I’d have to *add* a millisecond (or day or…) and for `after?` I’d have to *subtract*.

-a



--

Billy Lanchantin

unread,
Oct 31, 2022, 6:50:19 PM10/31/22
to elixir-lang-core
FWIW, I think a macro approach that takes a single argument and allows chained comparisons covers a lot of the cases being discussed here.

Consider something like:

# imports a compare?/1 macro
use CompareChain, for: DateTime

def between?(left, middle, right) do
  compare?(left <= middle < right)
end


The code reads well since you don't have the module name getting in the way. And it covers the annoying inclusive/exclusive issue quite nicely I think.

It's also convenient because I often find myself combining the results of comparisons (Ben provided some good examples). Being able to chain the operators within the macro helps avoids much of that verbose code. For instance, even with DateTime.before?/3 and DateTime.after?/3, you'd have to render my between?/3 as something like: 

def between?(left, middle, right) do
  DateTime.before?(left, middle, inclusive: true) and DateTime.after?(right, middle)
end

Cliff

unread,
Nov 1, 2022, 8:46:08 AM11/1/22
to elixir-lang-core
Would it be possible to allow different modules to define multiple clauses of the same function as long as they don't overlap? i.e. DateTime could define

defmodule DateTime do
  def %DateTime{ ... } >= %DateTime{ ... } do
    ...
  end
end


So that if you import DateTime, only: [:>=], a call to >= using DateTime structs would use DateTime.>=, and all other calls would match the clause for Kernel.>=?

Cliff

unread,
Nov 1, 2022, 8:55:14 AM11/1/22
to elixir-lang-core
Actually, using something like a Comparable protocol (as Jose mentioned) would do this using already-existing language features.

Cliff

unread,
Nov 1, 2022, 9:09:10 AM11/1/22
to elixir-lang-core
Okay, I'm totally in love with this idea :)

iex(1)>
defprotocol Comparable do
  @spec compare(t, t) :: :lt | :eq | :gt
  def compare(a, b)
end

defimpl Comparable, for: Integer do
  def compare(a, b) do
    cond do
      a < b -> :lt
      a == b -> :eq
      a > b -> :gt
    end
  end
end

defimpl Comparable, for: DateTime do
  def compare(a, b) do
    DateTime.compare(a, b)
  end
end

defmodule Foo do
  def a >= b do
    Comparable.compare(a, b) != :lt
  end
end
import Kernel, except: [>=: 2]
import Foo

It's magic!

iex(2)> 2 >= 1
true
iex(3)> DateTime.add(DateTime.utc_now(), 10, :hour) >= DateTime.utc_now()
true

José Valim

unread,
Nov 1, 2022, 9:26:17 AM11/1/22
to elixir-l...@googlegroups.com
Unfortunately general comparison requires multiple dispatch, because it is supposed to work regardless of the argument order.

For example, imagine that we provide a Comparable protocol and we implement it in Elixir for Integer and Float. Now, the Decimal library will also implement Comparable, and it also wants to work with Integer and Float. So we can handle:

Comparable.compare(decimal, integer)
Comparable.compare(decimal, float)

However, we can't handle:

Comparable.compare(float, decimal)
Comparable.compare(integer, decimal)

Because Elixir knows nothing about Decimal and I think not having that work breaks comparison in weird and surprising ways.

Now extend this to everything: you want it so Nx tensors can compare to integers and floats, as well as decimals. Then you will see a protocol solution indeed does not scale (code-wise).

What languages (both static and dynamic) typically do in cases without multiple dispatch is to provide a protocol for comparison. NumberComparable, DecimalComparable, TimeComparable, etc.

Wiebe-Marten Wijnja

unread,
Nov 1, 2022, 12:32:20 PM11/1/22
to elixir-l...@googlegroups.com

if you want to try out an example of this, I wrote an experimental implementation six years ago:
https://github.com/Qqwy/elixir-experimental_comparable

It has exactly the problem José describes: Defining a mutual comparison between `n` different datatypes requires `2*n*n` protocol module definitions.

An alternative approach some languages (such as Ruby) take, is to allow the definition of a coercion to a compatible datatype with sufficient detail.
This requires `n*(n-1)` definitions (one for each pair of different types, but order does not matter).
The result can be re-used for multiple operations rather than being hard-coded for comparison only.
An Elixir implementation of this can be found in https://github.com/qqwy/elixir-coerce
You can check out https://github.com/Qqwy/elixir-number for an example of usage. (Not for comparison but for calculation).

~Qqwy  / Marten

OpenPGP_signature

Billy Lanchantin

unread,
Nov 1, 2022, 6:45:39 PM11/1/22
to elixir-lang-core
I've written a proof of concept for the macro approach I described:


I opted for a compare?(expr, module) version. That way it reads like Enum.sort/2 and friends.

Some possible improvements:
  • Sanity checks to prevent things like 1 < 2 > 0
  • More options
  • Including expressions like compare?(a < b < c and d > e, DateTime)
    • You can always do compare?(a < b < c, DateTime) and compare?(d > e, DateTime), but it's still a lot of boilerplate
But I think it's a start. I may put it up on Hex after some polish.

Billy Lanchantin

unread,
Nov 5, 2022, 2:27:36 PM11/5/22
to elixir-lang-core
I ended up publishing on hex. Announcement of CompareChain:


I really liked the essence of Cliff's macro, so I built a version called compare? that also handles chained comparisons and logical operators. I think we're gonna get a lot of use out of it at CargoSense. Thanks to everyone here for the discussion and inspiration!
Reply all
Reply to author
Forward
0 new messages