[swift-dev] Rationalizing FloatingPoint conformance to Equatable

Jonathan Hull jhull at gbis.com
Tue Oct 31 16:37:27 CDT 2017


> On Oct 31, 2017, at 9:07 AM, Stephen Canon <scanon at apple.com> wrote:
> 
> [Replying to the thread as a whole]
> 
> There have been a bunch of suggestions for variants of `==` that either trap on NaN or return `Bool?`. I think that these suggestions result from people getting tunnel-vision on the idea of “make FloatingPoint equality satisfy desired axioms of Equatable / Comparable”. This is misguided. Our goal is (should be) to make a language usable by developers; satisfying axioms is only useful in as much as they serve that goal.
> 
> Trapping or returning `Bool?` does not make it easier to write correct concrete code, and it does not enable writing generic algorithms that operate on Comparable or Equatable. Those are the problems to be solved.
> 
> Why do they not help write correct concrete code? The overwhelming majority of cases in which IEEE 754 semantics lead to bugs are due to non-reflexivity of equality, so let’s focus on that. In the cases where this causes a bug, the user has code that looks like this:
> 
> 	// Programmer fails to consider NaN behavior.
> 	if a == b {
> 	}
> 
> but the correct implementation would be:
> 
> 	// Programmer has thought about how to handle NaN here.
> 	if a == b || (a.isNaN && b.isNaN) {
> 	}
> 
> W.r.t ease of writing correct *concrete* code, the task is to make *this* specific case cleaner and more intuitive. What does this look like under other proposed notions of equality? Suppose we make comparisons with NaN trap:
> 
> 	// Programmer fails to consider NaN behavior.  This now traps if a or b is NaN.
> 	// That’s somewhat safer, but almost surely not the desired behavior.
> 	if a == b {
> 	}
> 
> 	// Programmer considers NaNs. They now cannot use `==` until they rule out
> 	// either a or b is NaN. This actually makes the code *more* complicated and
> 	// less readable. Alternatively, they use `&==` or whatever we call the unsafe
> 	// comparison and it’s just like what we had before, except now they have a
> 	// “weird operator”.
> 	if (!a.isNaN && !b.isNaN && a == b) || (a.isNaN && b.isNaN) {
> 	}
> 
> Now what happens if we return Bool?
> 
> 	// Programmer fails to consider NaN behavior.  Maybe the error when they
> 	// wrote a == b clues them in that they should. Otherwise they just throw in
> 	// a `!` and move on. They have the same bug they had before.
> 	if (a == b)! {
> 	}
> 
> 	// Programmer considers NaNs. Unchanged from what we have currently,
> 	// except that we replace || with ??.
> 	if a == b ?? (a.isNaN && b.isNaN) {
> 	}

I still like both of these better than the first case, since the programmer has to take an action, which means they are forced to deal with the possibility in some way. Yes, that action could be mindless (like adding !), but at least there is an indication it could trap when reading the code.

I like your suggestion of making it reflexive better though...


> If we are going to do the work of introducing another notion of floating-point equality, it should directly solve non-reflexivity of equality *by making equality reflexive*. My preferred approach would be to simply identify all NaNs:
> 
> 	// Programmer fails to consider NaN behavior. Now their code works!
> 	if a == b {
> 	}
> 
> 	// Programmer thinks about NaNs, realizes they can simplify their existing code:
> 	if a == b {
> 	}

If you think this is possible/palatable, then this would be pretty ideal.


> What are the downsides of this?
> 
> 	(a) it will confuse sometimes experts who expect IEEE 754 semantics.
> 	(b) any code that uses `a != a` as an idiom for detecting NaNs will be broken.
> 
> (b) is by far the bigger risk. It *will* result in some bugs. Hopefully less than result from people failing to consider NaNs. The only real risk with (a) is that we get a biennial rant posted to hacker news about Swift equality being broken, and the response is basically “read the docs, use &== if you want that behavior”.

Maybe we can warn on 'a != a' as David suggests?  It is definitely a specific pattern that shouldn’t be used anywhere else, so we could write a pretty specific warning.


> One specific response:
> 
>> I see the handling of NaN as a legacy/compatibility issue due to committee/vendor politics from the 1980’s.  I am pretty sure if they could do it over with modern tech, we would just have isNan() and NaN == NaN… or we might just have optionals instead.
> 
> With the exception of how they interact with non-floating-point types (comparisons, conversions to/from integers and strings), NaNs are just Maybes with fast hardware support. Integers and booleans and strings are outside the scope of IEEE 754, so it was not in the standard’s purview to do anything else for those operations. They are not some exotic legacy thing leftover from the 1980’s; they were quite ahead of their time.

I hope I didn’t offend.  For what it is worth, I agree that NaN was ahead of it’s time, and they are essentially hardware supported optionals.  My point was if we were to do it from scratch in modern day, we might just use optionals, since we have those now.

The bit about a political/legacy issue was specifically about NaN != NaN, and not about NaN itself.  I seem to remember reading that there was a disagreement over whether to do NaN == NaN with .isNan() or NaN != NaN, and that the latter was chosen because one of the larger vendors wanted compatibility with their existing line, and pushed it through committee.  I can’t find the article now, so I could be mis-remembering.

Thanks,
Jon




More information about the swift-dev mailing list