Stream: API design

Topic: Num.min/max with floats


view this post on Zulip Richard Feldman (Oct 23 2023 at 02:31):

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

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:31):

this is because we have the naive implementation that just does a conditional

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:32):

so I can see an argument for 3 different potential designs for what we should do instead of this:

  1. always return NaN
  2. crash if either argument is NaN
  3. add an implements Eq constraint to min and max so you can't pass them floats anymore

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:33):

anyone have thoughts on which we should do?

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:39):

Definitely not 2 or 3

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:40):

I would either do 1 (most correct) or only return NaN if both are NaN (not correct but very useful in certain situations)

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:40):

But yeah, I would just do 1.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:41):

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.

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:41):

in defense of 3, wouldn't some of the same arguments apply here as applied when we decided not to give floats Eq? :thinking:

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:42):

I think 3 is bad cause comparing floats is super common. For example, which is faster?

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:43):

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.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:44):

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

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:48):

why definitely not 2?

view this post on Zulip Ayaz Hafiz (Oct 23 2023 at 02:49):

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"

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:50):

hm, interesting - we don't have Ord yet, but I was assuming we'd implement it for all numbers

view this post on Zulip Ayaz Hafiz (Oct 23 2023 at 02:50):

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

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:50):

so the args being Num would suffice there

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:52):

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)

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:54):

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

view this post on Zulip Ayaz Hafiz (Oct 23 2023 at 02:55):

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)

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:56):

Also, I think it is more reasonable to define min and max on floats than to define ordon them.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 02:57):

That said, I think they should have both (or at least min, max and PartialOrd)

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:57):

I definitely would prefer not to have both Ord and PartialOrd abilities in builtins :sweat_smile:

view this post on Zulip Richard Feldman (Oct 23 2023 at 02:58):

I think it makes sense for Rust to have them, but it doesn't seem great for Roc

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:00):

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.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:01):

With min and max. If two floats are approxEq the min or max of them will still be approxEq. So no harm done.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:02):

min and max don't get harmed by float values being super close but not exactly equal.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:02):

Ord does.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:03):

but yeah, float sorting with NaN falls appart because there is no clear place to sort NaNs to.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:03):

That is another case where Ord on floats is complicated.

view this post on Zulip Ayaz Hafiz (Oct 23 2023 at 03:04):

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.

view this post on Zulip Ayaz Hafiz (Oct 23 2023 at 03:05):

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

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:07):

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.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:08):

The only way the ieee gets a total ordering is by bitcasting to an integer (which roc doesn't allow users to do)

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:09):

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 do if 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:

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:09):

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.

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:10):

oh yeah, I forgot that sorting requires equality, which floats don't have

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:11):

(in current Roc I mean)

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:11):

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?

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:13):

oh yeah, I forgot that sorting requires equality, which floats don't have

Huh, no it doesn't

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:13):

Just < or >

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:14):

Would this but lead to a bug in most sorting algorithms?

Yes, yes it would

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:15):

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.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:15):

Min and max would follow that as well.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:16):

but that sounds unexpected and probably bugged in other ways.

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:17):

a relevant consideration is that we have Dec for reasonableness and floats for performance

view this post on Zulip Ayaz Hafiz (Oct 23 2023 at 03:18):

the total ordering isn't based on casting to integers. it's the "regular" ordering, with negative and positive NaNs pinnned to either ends.

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:18):

Yeah, that is the same as casting to a signed integer

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:19):

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

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:19):

or at least their main point

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:19):

*magnitude integer

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:19):

cause it doesn't have the twos compliment

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:19):

so yeah, sorry, a bit different

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:20):

from wikipedia:

the comparison is identical to one that type puns the floating-point numbers to a sign–magnitude integer

view this post on Zulip Brendan Hansknecht (Oct 23 2023 at 03:22):

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.

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:26):

haha yeah - basically error-prone APIs are more acceptable when it comes to floats

view this post on Zulip Richard Feldman (Oct 23 2023 at 03:26):

I should go to sleep :laughing:

view this post on Zulip Martin Stewart (Oct 23 2023 at 06:39):

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?

view this post on Zulip Martin Stewart (Oct 23 2023 at 13:20):

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)

view this post on Zulip Richard Feldman (Oct 23 2023 at 15:23):

yeah exactly

view this post on Zulip Richard Feldman (Oct 23 2023 at 15:23):

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