so in a recent Software Unscripted chat with Matt Godbolt, he noted that if min
or max
are given NaN
, it's important that they always return NaN
- we actually have a bug with this right now, where you get a different answer depending on the order of the arguments:
» Num.min (0f64 / 0) 1
1 : F64
» Num.min 1 (0f64 / 0)
NaN : F64
this is because we have the naive implementation that just does a conditional
so I can see an argument for 3 different potential designs for what we should do instead of this:
NaN
crash
if either argument is NaN
implements Eq
constraint to min
and max
so you can't pass them floats anymoreanyone have thoughts on which we should do?
Definitely not 2 or 3
I would either do 1 (most correct) or only return NaN if both are NaN (not correct but very useful in certain situations)
But yeah, I would just do 1.
Floats are meant explicitly to accept NaN in order to delay error handling and keep code fast. That is why I think 2 is bad.
in defense of 3, wouldn't some of the same arguments apply here as applied when we decided not to give floats Eq
? :thinking:
I think 3 is bad cause comparing floats is super common. For example, which is faster?
There are lots of cases for float ordering where you really don't care if two values are basically the same. If they are basically the same, either answer is really fine. Unlike with equality where being off by a little is super common and causes issues.
Like if 2 values are essentially equal and you call max, it really doesn't matter which value is return or if they are actually equal. So I think not the same as Eq
why definitely not 2?
3 should really be an implements Ord
constraint right? I think it's reasonable because floats don't have a total ordering. but the problem is you may get the very reasonable question "why is there no min for floats when I can do if a < b then a else if b < a then b else a
"
hm, interesting - we don't have Ord
yet, but I was assuming we'd implement it for all numbers
actually, apparently ieee 754 specifies one total ordering for floats, in which case 1 < NaN
can mean something: https://en.wikipedia.org/wiki/IEEE_754#Total-ordering_predicate
so the args being Num
would suffice there
although I certainly think there's an argument for those being consistent - like surely if we define Ord
for floats, then Num.min
and Num.max
should be available on them (or else none of them should be available)
For 2, NaN is often expected. It means that the rest of a float computation is invalid. So you let the NaN propagate to the end like you would a result (just faster). At the end of the calculations, you handle the NaN. Crashing on NaN would ruin the usability
well, it depends what Ord
is. Is it a total ordering? If so then yeah, certainly min/max must be available. But if Ord is defined such that a < b
being false does not imply that a >= b
is true, then it's a little bit more free (but at that point you probably want to have something like rust's partial Ord too)
Also, I think it is more reasonable to define min
and max
on floats than to define ord
on them.
That said, I think they should have both (or at least min
, max
and PartialOrd
)
I definitely would prefer not to have both Ord
and PartialOrd
abilities in builtins :sweat_smile:
I think it makes sense for Rust to have them, but it doesn't seem great for Roc
The reason min
and max
are more reasonable than Ord
on floats is the potential damage that can be done. With Ord
, you will hit cases where two floats are approxEq
, but Ord
gives them a strict ordering. This can lead to mistakes where a user actually wants to treat to approxEq
floats as Eq
, but doesn't realize that by using Ord
everything is exact instead of approximate.
With min
and max
. If two floats are approxEq
the min
or max
of them will still be approxEq
. So no harm done.
min
and max
don't get harmed by float values being super close but not exactly equal.
Ord
does.
but yeah, float sorting with NaN
falls appart because there is no clear place to sort NaNs
to.
That is another case where Ord
on floats is complicated.
why is there no clear place? we could follow the ieee 754 standard, or come up with another one. I don't think there's a technical limitation there.
I don't think Ord is a concern for floating points if you only allow a strict order, that is, only comparing a < b
rather than a <= b
(which is what min/max already do). I think we cannot do a <= b
-style total ordering anyway, because otherwise floats can implement Eq via Ord, which is incorrect
There is no clear place because if you implement a userland sort using <
or >
you will get strange results when NaN
is in a the array. It depends on if the comparisons are x < NaN
or NaN < x
.
The only way the ieee gets a total ordering is by bitcasting to an integer (which roc doesn't allow users to do)
Ayaz Hafiz said:
3 should really be an
implements Ord
constraint right? I think it's reasonable because floats don't have a total ordering. but the problem is you may get the very reasonable question "why is there no min for floats when I can doif a < b then a else if b < a then b else a
"
another way of thinking of this: if we don't support Num.min
and Num.max
on floats, then people will probably implement their own in the naive way and accidentally end up with the same bug we did :grimacing:
So multiple "correct" strict ord based sorting algorythms in roc would put NaNs
in different places. This can even happen with the same alg if NaN
is just in a different place in the array.
oh yeah, I forgot that sorting requires equality, which floats don't have
(in current Roc I mean)
relevant previous discussion:
Brendan Hansknecht said:
obviously
x < NaN == NaN < x
is unintuitive, but can we think of some specific realistic scenarios where this would cause a bug?Would this but lead to a bug in most sorting algorithms?
oh yeah, I forgot that sorting requires equality, which floats don't have
Huh, no it doesn't
Just <
or >
Would this but lead to a bug in most sorting algorithms?
Yes, yes it would
I mean, I guess if we want to follow the float total ordering standard, we would change all comparison operators and floats to treat them as signed integers.
Min and max would follow that as well.
but that sounds unexpected and probably bugged in other ways.
a relevant consideration is that we have Dec
for reasonableness and floats for performance
the total ordering isn't based on casting to integers. it's the "regular" ordering, with negative and positive NaNs pinnned to either ends.
Yeah, that is the same as casting to a signed integer
so there's a case to be made that our tolerance for error-prone-ness in floats should be reduced for the sake of increasing performance, since that's kind of their whole point
or at least their main point
*magnitude integer
cause it doesn't have the twos compliment
so yeah, sorry, a bit different
from wikipedia:
the comparison is identical to one that type puns the floating-point numbers to a sign–magnitude integer
our tolerance for error-prone-ness in floats should be reduced for the sake of increasing performance
Can you word that differently? At least as I am reading it, I think it is backwards from what I feel you are trying to say.
haha yeah - basically error-prone APIs are more acceptable when it comes to floats
I should go to sleep :laughing:
Maybe I misunderstood the decision made in an earlier thread or maybe plans have since changed but I thought Roc wasn’t going to allow NaN to exist in order to avoid these sorts of issues. Any functions that could potentially return NaN would either panic or return an error if it were to happen?
https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/supporting.20NaN.2FInfinity.2F-Infinity.3F Found the discussion. Not sure what conclusion was made though (I guess it was to keep NaN given it's assumed here)
yeah exactly
the problem was that if we don't have it in the language, there's no way to get maximum float performance - and the main point of having floats at all is performance!
Last updated: Jul 06 2025 at 12:14 UTC