nil
is annoying for CompareChain
and I would like it to be better. I’m looking for some feedback on approaches!
The problem
compare?/2
works quite well for reducing boilerplate when all the arguments are non-nil
. But when nil
is a possible value it leads to headaches.
At CargoSense, we currently have 4 separate comparison modules to deal with this problem:
DateTimeNilInfinity
-nil > datetime
andnil == nil
DateTimeNilNegInfinity
-nil < datetime
andnil == nil
DateTimeNilInfinityRaise
-nil > datetime
andnil
raises if compared tonil
DateTimeNilNegInfinityRaise
-nil < datetime
andnil
raises if compared tonil
It’s… ok. It deals with the problem in a succinct-ish way:
# Before
old && new && DateTime.compare(old, new) != :eq || old != new
# After
compare?(old != new, DateTimeNilInfinity)
The problem is that no one wants to read DateTimeNilNegInfinityRaise
, let alone think though all the casework to understand what the code means.
I have considered a few options over the years. @benwilson512 recently advocated for another one but I remain undecided.
Options
1. SQL-like optional argument
We make CompareChain
aware of nil
s and handle them specially.
compare?(old != new, DateTime, nils: :infinity)
nils
can be one of: :infinity
, :neg_infinity
, :infinity_raise
, :neg_infinity_raise
. This is reminiscent of how SQL allows you to specify NULLS FIRST | LAST
.
Downsides:
- All
nil
s would be necessarily be treated the same in a single expression. - All desired ways of handling
nil
would need to be represented as one of the options. (I.e. the 4 I listed may not be exhaustive.)
2. inf()
and -inf()
(Courtesy of @benwilson512)
We provide expressions which CompareChain
understands how to handle natively:
compare?(old || inf() != new || inf(), DateTime)
# or possibly
compare?(inf(old) != inf(new), DateTime)
Here we have -inf() < datetime < inf()
(or whatever the struct is), inf() == inf()
, and -inf() == -inf()
. Then nil
could be handled in the “normal” way of raise
ing if passed to a function like DateTime.compare/2
. And if you didn’t want that, you provide inline fallbacks.
Downsides:
- Still a little verbose (and possibly hard to understand if we go with
inf(old) != inf(new)
)
Other considerations
-
Current approach of defining bespoke modules automatically interopts with
Enum.sort/2
and friends. The new approaches would not. (Though one could still define a bespoke module if one wanted.) -
Certain 3-valued logics are simply impossible to encode using
compare/2 #=> :lt | :eq | :gt
. E.g.select 1 = null;
in SQL returnsnull
, meaning none of:lt | :eq | :gt
are appropriate. We’ve sort of been handling this byraise
ing internally. But just note that the design space forCompareChain
is fundamentally limited in this regard since the intent is to interop with the existingcompare/2
paradigm.
Feedback
Please LMK your thoughts! Given that CompareChain
is so conceptually simple, I’m hesitant to add special casing. But nil
really is annoying, so I’d love to hear any opinions.
Also please advocate for another approach you like if you think of one!