Why can't I compare DateTime with '>' or '<'?

196 views
Skip to first unread message

최병욱

unread,
Mar 2, 2023, 10:38:00 AM3/2/23
to elixir-lang-core
Can't you compare DateTime with '>' or '<' instead of DateTime.compare?

Jay Rogov

unread,
Mar 2, 2023, 10:54:08 AM3/2/23
to elixir-lang-core
Because the underlying structure used to represent DateTime is a struct, which is simply a map under the hood.
Erlang/Elixir uses a rather arbitrary order of keys (e.g. hour -> year -> day -> minute) when comparing 2 maps which you can't control.

Thus, you need to have a specific function that would compare these structs according to implied field order (year -> month -> day -> hour -> etc.)

More: https://hexdocs.pm/elixir/main/NaiveDateTime.html#module-comparing-naive-date-times

Billy Lanchantin

unread,
Mar 2, 2023, 8:03:25 PM3/2/23
to elixir-lang-core
Shameless plug: I wrote a library called `CompareChain` that allows you to use operators like `<` and `>` on structs like `DateTime`.

최병욱

unread,
Mar 2, 2023, 10:42:23 PM3/2/23
to elixir-lang-core
So Why don't we implicitly sort it so that it can be compared by inequality sign(> or <)?

2023년 3월 3일 금요일 오전 10시 3분 25초 UTC+9에 william.l...@cargosense.com님이 작성:

Austin Ziegler

unread,
Mar 3, 2023, 1:47:09 AM3/3/23
to elixir-l...@googlegroups.com
In this case, because Elixir is passing the `<` and `>` comparisons to the underlying BEAM operations and there’s no overloading to say that `left < right` should mean `DateTime.compare(left, right) < 0` and `left > right` should mean `DateTime.compare(left, right) > 0` (if I’m remembering `DateTime.compare/2` correctly).

`CompareChain` does that, but it’s something that gets opted into.

-a

--
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/afa3830a-8944-4e12-84cc-d8e28d9fceb0n%40googlegroups.com.


--

José Valim

unread,
Mar 3, 2023, 3:27:00 AM3/3/23
to elixir-l...@googlegroups.com
It is also important to note that both kinds of comparisons are important to have in a language. The docs for main discuss this: https://hexdocs.pm/elixir/main/Kernel.html#module-structural-comparison

Billy Lanchantin

unread,
Mar 3, 2023, 9:31:28 AM3/3/23
to elixir-lang-core
> if I’m remembering `DateTime.compare/2` correctly

Close! The `Module.compare/2` functions return one of `:lt`, `:eq`, or `:gt` ("less than", "equal to", "greater than"), similar to what Haskell does. You may have been thinking of something like OCaml where `compare` returns `-1`, `0`, or `1` resp.


> So Why don't we implicitly sort it so that it can be compared by inequality sign(> or <)?

To clarify, functions like `<` define the sort order.

Any time you sort a list, you're using a function that compares two elements. Even if you call `Enum.sort/1`, you're implicitly using `<=/2` as the comparison function. If you want some other sort order, e.g. for semantic ordering of `DateTime`s, then you must supply your own comparison function.

The reason that you can use `<` on structs with `CompareChain` is that it uses macros to re-write an expression like

`~D[2023-03-03] < ~D[2023-03-04]`

as

`Date.compare(~D[2023-03-03], ~D[2023-03-04]) == :lt`.

But that doesn't change the behavior of `<` itself. We're basically stuck with what `<` and the like do. Though as José points out, that's actually a good thing.

(Side note, you actually have to call `compare?(~D[2023-03-03] < ~D[2023-03-04], Date)` with `CompareChain` to invoke the re-write. I just wanted the example to be more readable.)

Marc-André Lafortune

unread,
Mar 3, 2023, 11:26:19 AM3/3/23
to elixir-lang-core
It's great that there exists a total order (structural) in Elixir/Erlang, I just wish it wasn't accessible with `<`, `>`, as it is too error prone and is simply never what one wants to do (at least in our app). Elixir 2.0? 😆

At work I just recently overloaded them to raise unless both arguments are `is_number`, and we found bugs where we were comparing Decimals, and other bugs where we were comparing with `nil`. They are no longer allowed in guards too.

José Valim

unread,
Mar 3, 2023, 11:43:13 AM3/3/23
to elixir-l...@googlegroups.com
If we have a type system, we will 100% warn in those cases. :) crossing fingers.

Sabiwara Yukichi

unread,
Mar 4, 2023, 1:32:25 AM3/4/23
to elixir-l...@googlegroups.com
> It's great that there exists a total order (structural) in Elixir/Erlang, I just wish it wasn't accessible with `<`, `>`, as it is too error prone and is simply never what one wants to do (at least in our app). Elixir 2.0? 😆

(another shameless plug) Your comment motivated me to release this project I was working on: https://github.com/sabiwara/cmp.
Feedback welcome :)

José Valim

unread,
Mar 4, 2023, 3:00:41 AM3/4/23
to elixir-l...@googlegroups.com
We had discussions in the past and the issue with a Comparable protocol is that we need multiple dispatch. For example, we should be able to semantically compare "Integer cmp Decimal" and "Decimal cmp Integer" which is a more complex problem as it requires defining a scale to compare all of them. Then you can add a compare numbers functionality that converts them to said scale using a separate protocol. It will still require at least two protocol dispatches.

Sabiwara Yukichi

unread,
Mar 4, 2023, 4:34:21 AM3/4/23
to elixir-l...@googlegroups.com
Thank you José for the feedback!

I considered this point, and although it would be ideal, I decided to consider this case an acceptable trade-off not to handle it, because:
1. it would make the implementation much more complex as pointed out
2. it would remove a lot of potential for optimizations
3. it might not be such a common huge case, because programs tend to work with a given type, mixing them is not so common

I'm a) guarding against number-decimal comparisons and b) handling semantic decimal-decimal comparisons, which should cover the two main pitfalls with decimals in my experience:
iex> max(Decimal.new(2), Decimal.from_float(1.0))
#Decimal<1.0>
iex> Cmp.max(Decimal.new(2), Decimal.from_float(1.0))
#Decimal<2>

iex> Cmp.max(Decimal.new(2), 1.0)
** (Cmp.TypeError) Failed to compare incompatible types - left: #Decimal<2>, right: 1.0

Sabiwara Yukichi

unread,
Mar 4, 2023, 4:39:55 AM3/4/23
to elixir-l...@googlegroups.com
Also, even Decimal.compare/2 choses to support this which comforted me with this decision:

iex> Decimal.compare(Decimal.new(2), 1.0)
** (ArgumentError) implicit conversion of 1.0 to Decimal is not allowed. Use Decimal.from_float/1

José Valim

unread,
Mar 4, 2023, 4:43:08 AM3/4/23
to elixir-l...@googlegroups.com
I makes sense to not compare decimals and floats but maybe compare accepts decimals and integers?

Reply all
Reply to author
Forward
0 new messages