billylanchantin

billylanchantin

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 and nil == nil
  • DateTimeNilNegInfinity - nil < datetime and nil == nil
  • DateTimeNilInfinityRaise - nil > datetime and nil raises if compared to nil
  • DateTimeNilNegInfinityRaise - nil < datetime and nil raises if compared to nil

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 nils 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 nils 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 raiseing 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 returns null, meaning none of :lt | :eq | :gt are appropriate. We’ve sort of been handling this by raiseing internally. But just note that the design space for CompareChain is fundamentally limited in this regard since the intent is to interop with the existing compare/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!

Showing Posts 1 to 3

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

To further advocate for this a bit I think it’s worth looking at how it handles more “normal” CompareChain scenarios since this != one is actually less common, and using the normal DateTime.compare? might actually be fine in this situation eg if new && old && DateTime.compare?(new, old) != :eq do

Consider:

if compare?(data.sync_cursor >= shadow.sync_cursor, DateTimeNilNegInfinity) do
  # stuff
end

What we are trying to deal with is that shadow.sync_cursor might be nil if the shadow is freshly initialized. In these cases we want nil to evaluate as less than any possible value of new_data.sync_cursor which is coming in. So in my proposal you’d have:

if compare?(data.sync_cursor >= shadow.sync_cursor || -inf(), DateTime) do
  # stuff
end

or

if compare?(data.sync_cursor >= -inf(shadow.sync_cursor), DateTime) do
  # stuff
end

to indicate that negative infinity is being used as a fallback for the value of shadow.sync_cursor.

This really improves the look I think of chained comparisons too:

compare?(si.activated_at <= geo_elem.latest_recorded_at <= si.deactivated_at || inf(), DateTime)

Because in this particular case we are being very explicit about which of the possible values we want to be OK with being nil.

billylanchantin

billylanchantin OP

@benwilson512 Yeah great callout. An upside of an inline approach is that individual nils can be special cased. I didn’t do a good job highlighting that in the OP.

bruce

bruce

Author of Craft GraphQL APIs in Elixir with Absinthe

I’m a fan of the inf() approach for its flexibility.

Nit (naming): I think I’d use infinity(), personally. I think the keystrokes are worth the clarity, and I’ve seen :infinity used for case matching before. (:hot_pepper: : inf feels like an unwarranted homage to Python’s math.inf.)

— All posts loaded —

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 92995 915
New
caslu
I want to open this thread for you all to discuss and help those who really like Ash but are still hesitant to use it in a real project. ...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
GES233
I’m posting this in response to Jose’s recent tweet (Cr. link) : People are sleeping on Elixir for a coding harness: Hot-code swappi...
New
_mfierro
Hello, I wrote Stop My Hand, a Scattergories-like web application using Phoenix/LiveView as my learning project for Elixir (after readin...
New
marciol
It would be helpful to have a list of companies worldwide that hire engineers without prior experience in Elixir. Often, it can be quite ...
New
durvia
Anyone running long-lived stateful processes on BEAM? We’re building an AI agent runtime and would love to compare notes. We’re a small ...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews