Maps are ordered by size, two maps with the same size are compared by keys in ascending term order and then by values in key order. In maps key order integers types are considered less than floats types.
I know this is an old topic and I understand that using < does structural comparison.
But I just discovered a bad bug in my application that was super nasty to track down.
I had first_entry = entries |> Enum.min_by(& &1.inserted_at) somewhere in critical business logic.
Obviously, this code is wrong, it should be:
Structural comparison of dates was actually used as an example of the new type system when it was introduced! At the moment I don’t believe it can catch this within a function like Enum.min_by() (obviously since it didn’t for you), but there are more type system changes coming in 1.19 and 1.20 which have to do with functions, so perhaps it will eventually?
Honestly? I think this is just one of those things you need to be paranoid about.
Comparing structs falls into an unfortunate local minima of Elixir’s design. There is a total ordering on its terms, so a < b is always valid. So the compiler is a bit limited in what it can do. Though as @garrison noted, you do get warnings in certain circumstances.
However, unless the compiler knows for some reason that the result of & &1.inserted_at will always be a %NaiveDateTime{}, there’s nothing for it to do. You could maybe do:
But at that point, you’re bending over backwards to get the compiler to tell you something you clearly already know. Though maybe if you use TypedEctoSchema, the compiler would be able to infer.
One other thought: always consider adding an order_by clause to your queries. Similar to this particular gotcha, it’s very easy to unknowingly rely on the order a query even though you didn’t specify it. E.g. we had a series of heisenbugs in our tests where I was blithely asserting on the result of Repo.all(). The results rows usually ordered the same, but not always.
Oh that’s great, I must have missed that.
I’ll need to do some experiments to see under which circumstances it can catch these things.
I’m already on 1.19.0-rc.2, so maybe if add some pattern matches on the Repo.all() results it might catch this.
It looks like TypedEctoStruct helps to create dializer types, but I think those are ignored in the new type system, so this won’t help.
Also, I think the source of the problem is that it can’t know the type of Repo.all.
But I guess in theory it would be possible to infer the type from the query, so maybe the compiler can do it in the future.