Rationalizing FloatingPoint conformance to Equatable

+1 for trapping unless using &==. In the case of ‘Float?’ we could also
map to nil.

This is probably a more appropriate discussion for evolution though...

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it
cannot be migrated to `&==` if it were felt that `==` *must* be a full
equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but
my opinion is that NaNs are so exceptional, and so prone to misuse, that we
ought to treat them like integer arithmetic overflows: trap when they're
detected, unless you use an `&` variant operator which indicates you know
what you're doing.

I strongly suspect that, in practice, most float-manipulating code is
not prepared to handle NaN and will not do anything sensible in its
presence. For example, Apple platforms use floating-point types for
geometry, color components, GPS locations, etc. Very little of this code
will do anything sensible in the presence of a NaN. Arguably, it'd be
better to exclude them through the type system, but I don't think that's a
realistic possibility—we would need to have done that in a more
source-break-friendly era. But that doesn't have to mean we're completely
stuck.

Built-in floating point operators, as well as libc/libm math functions,
are designed to propagate NaN correctly. This is not meant to be a thread
about NaN, and we need to be cautious to define the scope of the problem to
be solved from the outset. The tendency of having ever-expanding discussion
where issues such as method names turn into discussions about the entire
standard library go nowhere.

The question here is about `==` specifically and how to accommodate
partial equivalence relations. For sanity, we start with the premise that
NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve
self-consistency and simplicity, then the tradeoff might be worth it. The
alternative, teaching the Equality protocol about NaNs, feels like “the
tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from
IEEE’s opinions about language/library design.

IEEE 754 requires certain behaviors of conforming implementations and is
meant to allow for portable use of floating point. Swift’s floating point
facilities are conformant to that standard, and breaking IEEE 754
conformance has been repeatedly stated to be a non-starter.

The essential idea behind quiet NaN is that it can be used as input in
operations that take floating point values without raising errors. Since
breaking IEEE 754 conformance is a non-starter, trapping on NaN is outside
the realm of available solutions.

It has been pointed out, however, that IEEE does not require a specific
syntax for floating point equivalence, hence the question of whether it can
be spelled differently. However, trapping on NaN is emphatically not an
option.

But I really don’t want to discuss this at all here. The topic of this
thread is about the semantics of Equatable.

On the other hand, changing the spelling of IEEE `==` was option in your
opening post represented by demand 4 and question D.

4) IEEE-compliant floating-point equivalence must be spelled `==` and not
some alternative such as `&==`.

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it
cannot be migrated to `&==` if it were felt that `==` *must* be a full
equivalence relation. I believe this is controversial, however.

What you call the "proximate issue” goes away if the solution to what you
call the “distal issue” involves requiring `Equatable` conforming types to
provide an implementation of `==` that represents a full equivalence
relation. It looks to me like many people are suggesting that this is
exactly what we should do.

I not a numerics expert by any means and am therefore cautious to offer my
opinion, but rejecting demand 4 appears to be the best option to me. One
advantage of this approach in addition to providing a full equivalence
relation for floating point’s equatable conformance is that it makes it
clear when an algorithm was written with IEEE semantics in mind or when the
programmer did not desire or may not be aware of IEEE semantics.

I agree.

IMO, the more general issue of partial equivalence relations should be

orthogonal. I don’t believe NaN should drive the semantic requirements of
`Equatable` and I don’t believe IEEE should prevent floating point types
from providing an Equatable conformance.

I think this is reasonable as well.

Perhaps this is a sign of my ignorance of numerics, but that’s the solution

this seems to make the best tradeoffs. I am happy to let others decide
whether `NaN == NaN` should trap or return `true`.

Trapping violates the requirements of Equatable and, I think, also IEEE
edicts; NaN == NaN would be the only solution if we dispense with demand 4.

···

On Fri, Oct 20, 2017 at 10:10 Matthew Johnson <matthew@anandabits.com> wrote:

On Oct 20, 2017, at 9:36 AM, Xiaodi Wu via swift-dev <swift-dev@swift.org> > wrote:
On Fri, Oct 20, 2017 at 07:21 David Zarzycki <dave@znu.io> wrote:

On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org> >> wrote:
On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com> wrote:

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev < >>> swift-dev@swift.org> wrote:
On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev < >>> swift-dev@swift.org> wrote:

_______________________________________________

swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

The performance aspect of this issue are daunting, and I'm glad you brought
it up. On a cursory reading, having every NaN value compare equal to every
other NaN value is likely to be several times, if not orders of magnitude,
slower than the hardware IEEE implementation. This would be an
extraordinary cost and makes me wonder if this is at all a good idea. It
would serve us well to re-evaluate what generic algorithms absolutely
require a full equivalence relation. Let's take a look at `Array`, for
example.

- `index(of:)` works perfectly sensibly without such a relation; if no NaN
is equal to any other NaN, `index(of: .nan)` is appropriately `nil`.
- `min()` and `max()` are something else entirely, as we're talking here
only of equivalence and not of total order, which is another issue
altogether.
- `sort()` is problematic, but not if a custom predicate is supplied.
- `split()` only runs into problems if specifically trying to split a
sequence on `.nan`, but again this would be unsurprising if no NaN is equal
to any other NaN.
- `==` is broken but can be fixed as shown in PR #12503.

···

On Fri, Oct 20, 2017 at 2:42 PM, Stephen Canon <scanon@apple.com> wrote:

On Oct 20, 2017, at 8:21 AM, David Zarzycki via swift-dev < > swift-dev@swift.org> wrote:

On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org> > wrote:

On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com> wrote:

+1 for trapping unless using &==. In the case of ‘Float?’ we could also
map to nil.

This is probably a more appropriate discussion for evolution though...

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev < >> swift-dev@swift.org> wrote:

On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org> >> wrote:

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it
cannot be migrated to `&==` if it were felt that `==` *must* be a full
equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but
my opinion is that NaNs are so exceptional, and so prone to misuse, that we
ought to treat them like integer arithmetic overflows: trap when they're
detected, unless you use an `&` variant operator which indicates you know
what you're doing.

I strongly suspect that, in practice, most float-manipulating code is not
prepared to handle NaN and will not do anything sensible in its presence.
For example, Apple platforms use floating-point types for geometry, color
components, GPS locations, etc. Very little of this code will do anything
sensible in the presence of a NaN. Arguably, it'd be better to exclude them
through the type system, but I don't think that's a realistic
possibility—we would need to have done that in a more source-break-friendly
era. But that doesn't have to mean we're completely stuck.

Built-in floating point operators, as well as libc/libm math functions,
are designed to propagate NaN correctly. This is not meant to be a thread
about NaN, and we need to be cautious to define the scope of the problem to
be solved from the outset. The tendency of having ever-expanding discussion
where issues such as method names turn into discussions about the entire
standard library go nowhere.

The question here is about `==` specifically and how to accommodate
partial equivalence relations. For sanity, we start with the premise that
NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve
self-consistency and simplicity, then the tradeoff might be worth it. The
alternative, teaching the Equality protocol about NaNs, feels like “the
tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from
IEEE’s opinions about language/library design.

Just to be precise: IEEE 754 places no requirements on hardware. The
entirety of IEEE 754 is about what *languages* should provide. It just
happens to be advantageous to implement many of the requirements directly
in hardware.

[The rest of this is a response to the thread as a whole, not to Dave]

I have no philosophical objection to trapping on NaN. IEEE 754 says that
the default behavior should be to not trap, but other non-default forms of
exception handling* are explicitly allowed by IEEE 754.

From a practical standpoint, it’s is counter to everything about the way
much floating-point hardware is designed, and that should give us some
pause. On x86 it’s possible to unmask the “invalid floating point
exception”, which results in any operation that generates a NaN faulting.
However, it will *not* cause a fault if an operand is already a quiet NaN,
so Swift would need to either test every operand that’s read from memory at
the time that it’s moved into register, or test every result.

On some non-x86 architectures (including in particular most ARM
implementations) there is no hardware support for unmasking exceptions, so
there’s no way to automatically trap on invalid operations, you would have
to explicitly check for NaN on every operation. This is much, much more
expensive than checking for overflow on integer arithmetic (where for
addition / subtraction, it’s just an easily-predicted conditional branch).
Including these checks would introduce significant code bloat and slow down
naive arithmetic by roughly an order of magnitude on current hardware,
which is probably a non-starter.

Trapping only for == is much, much more palatable, but as Xiaodi said,
doesn’t actually get you the semantics that you want for ==.

&== is ugly but workable. You will have inevitable bugs from people who
naively adapt code from literally any other language that assumes IEEE 754
semantics for ==, however.

– Steve

[*] note that “exception handling” in an IEEE 754 context does not mean
what you think it does if you’re coming from a generic CS
not-floating-point background.

One possibility which might be easier would be to just depreciate Float’s Equatable conformance and provide a warning/fixit (suggesting how to use the new ‘~’ relation).

···

On Oct 25, 2017, at 2:35 AM, Jonathan Hull <jhull@gbis.com> wrote:

There is something interesting here. I like that ‘Bool?’ really represents what we need from the relation on Float. I think it solves the objection that Xiaodi mentioned with a PartialEq protocol. If everyone just switches to using PartialEq in generic contexts because they want it to work with Floats (which is the outcome he was worried about)… then they will be forced to deal with the optional! This brings Swift’s strengths into play, and we will have gained correctness.

The issue is migration though. Let’s say we name the partial equivalence relation ‘~’ and then we remove Float’s Equivalence conformance (no more == for Float). To migrate, we would need to replace any ‘a == b’ where there is a Float with ‘(a ~ b) == true’. Not pretty, but not impossible.

The trouble spot will be where someone has written their own method which is generic on Equatable, and they are passing Floats to it. We need to be careful about the warning given, so that it sends them in the right direction. The good news is that if they do rewrite the function to use ‘~' it should actually cause them to fix bugs which were there (or at least consider the case of NaN) when dealing with Floats. So, overall good things, but a bit annoying while transitioning.

We should keep thinking/discussing. I am convinced there is an interesting thread to pull on here...

On Oct 24, 2017, at 11:25 PM, David Sweeris via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 24, 2017, at 9:06 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Tue, Oct 24, 2017 at 10:08 PM, Ben Cohen <ben_cohen@apple.com <mailto:ben_cohen@apple.com>> wrote:

On Oct 24, 2017, at 6:48 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

On Tue, Oct 24, 2017 at 1:55 PM, Ben Cohen <ben_cohen@apple.com <mailto:ben_cohen@apple.com>> wrote:

On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

Differing behavior in generic and concrete contexts is simply too subtle to be understandable to the reader.

Hardly more subtle then the current “Equatable works like this, with these strong guarantees. Oh, except for some cases it doesn’t, in which case ¯\_(ツ)_/¯”

I'm not saying that the status quo is a superior alternative.

However, one option is to _weaken_ the guarantees of Equatable such that it guarantees only partial equivalence for `==`. From the perspective of documented semantics, it's not subtle at all but a giant hammer of a change. However, from an actual what-does-the-implementation-do standpoint, it would be acknowledging what is already true. Only code that is already broken when used with floating-point values would become formally "incorrect" in the sense of relying on semantics that are then no longer guaranteed.

Such a solution would avoid, as you might say, perpetuating the ¯\_(ツ)_/¯ approach to floating point.

I realize that Comparable admits an exception for FP. This is, IMO, a serious mistake and needs to be reversed. Equatable has no such exception and rightly so.

The clearest demonstrations of how flawed this approach is can be found in the Standard Library. You can throw a brick at it and hit an example of something that’s broken by the presence of .nan: random sort orders, == implementations that differ based on the identify of the buffer,

In my view, if a sort algorithm cannot accommodate NaN, it's entirely acceptable to trap on NaN--and that is a trivial change.

I think this would be user-hostile. This isn’t like out-of-bounds subscript where it’s just not possible to reasonably proceed. NaNs crop up and people don’t expect them to trap when you sort – they expect them to sort to one end, like in Excel.

Honestly, I don't know that most users have thought about this possibility at all. Sure, a sort that matches IEEE total order _might_ be justifiable. But users are as likely to expect that the last item in the sorted collection will be the greatest and that the first item in the sorted collection will be the smallest. Now, you can say that NaN compares larger than everything, everywhere. But the moment that they try to plug that last element into, say, an AppKit UI function, they're toast.

I certainly disagree with ideas of trapping on NaN inside `==` or similar functions, but I really do think that an argument can be made that it is not reasonable to proceed with sorting an array that contains NaN.

After all, NaN is unordered with respect to everything and one cannot sort the unsortable. And, as shown, the `Array.==` implementation is trivially fixable. The entire standard library can be made NaN-safe in like manner.

My point was, it’s not about what we can do in the standard library. The std lib only has a handful of methods and sure, we can fix them one by one. It’s about whether the standard library defines types and protocols such that it’s reasonable for programmers to use them to write and use generic algorithms correctly. I’m citing the existing std lib implementations as proof that it’s easy to make mistakes. And I think a more complicated approach, with more operators, more properties, more rules, won’t fix this problem.

Well, to my mind, this problem you state really works out to:

(a) People expect generic algorithms that operate on Comparable types to work correctly with floating-point types
(b) Generic algorithms that operate on Comparable types don't work correctly with floating-point types unless the author is very, very careful
(c) People shouldn't have to be very, very careful to write working generic algorithms that work with floating-point types

Which, in turn, really boils down to:

(d) People expect floating-point types not to have numerous unintuitive (but learnable) properties, including NaN being unordered
(e) Floating-point types have numerous unintuitive (but learnable) properties, including NaN being unordered

The reason I'm writing to swift-dev (rather than evolution) is that my interest is in fixing the standard library. I'm not even convinced that this problem you state is fixable, at least on your terms. In the interest of not increasing the API surface area, you would propose to blow away (e) in the generic but not concrete context. Now, while it's true that an alternative to increasing the API surface area is to have the same API exhibit context-specific behaviors, that certainly isn't any less complicated conceptually, as we would then be faced with the notion that floating-point types both have and do not have numerous unintuitive properties, depending on the context in which they are used.

arbitrary duplication in Set/Dictionary etc.

(I disagree that it's arbitrary. If NaN != NaN, then every NaN is properly unique.)

The important point to take from this is not “how do we fix the Standard Library?” but rather “these errors are easy to make” by anyone writing generic code using standard protocols. If the Standard Library can’t get these right, how can we expect others to? There are potentially far worse bugs that could result. A differently-written sorting algorithm could corrupt elements (because it relied on substitutability). Other sorting or searching algorithms could easily go into an infinite loop. These problems exist because the code relies on the documented behavior of the protocol, because if you can’t, then what is the point in documenting that behavior?

It's not that the standard library *can't* get these right, but that it currently *doesn't*, because it documents one set of semantics but implements another, then relies on documented semantics that it knows it does not implement. We both agree that this needs to be fixed.

The question here is whether it is to be fixed by sticking to the documented semantic guarantees of `==` and bringing all implementations into proper conformance, or alternatively sticking to the implemented behavior of `==` and aligning the documented semantic guarantees to that.

I don’t support solutions such as adding a property indicating “containsExceptionalValues” (whatever that means), and expecting every author of a generic algorithm that uses Equatable to remember to call it, and craft custom paranoid behavior (if there is any reasonable behavior) based on it. With recursive conformance landed on master, we finally have a generics system where writing algorithms against Collection can be considered approachable by ordinary users. You no longer have to know things like how Collection.SubSequence needs to be constrained to also be a Collection – it just is. We would be negating this good work to now introduce a whole new set of gotchas that we expect people to know (without the type system even helping them in this case) about how some types, including standard library types, flout the documented rules for Equatable and Comparable, and that you need to use one of a handful of properties to hack in special cases to handle it.

The gotchas aren't new; they arise when using floating point values, originate with the IEEE definition of floating point equivalence, and exist in some form in every language that has implemented collections of floating point values. Crucially, they exist today in Swift; only, we haven't documented it.

And as a user of algorithms, what should you do? If a generic algorithm doesn’t document how it handles these special cases, should you assume it doesn’t? Check the code? Experiment to find out?

This problem also spreads, virus-like, once we have conditional conformance that makes containers equatable when their elements are. [Double] would need to propagate it’s elements’ “exceptionality", to avoid problems with [Double]. Double? will have to do the same.

This isn't a _problem_. In fact, I consider this to be a key _feature_. Naturally, every protocol conformance (conditional or not) must implement all protocol requirements, so if we add additional requirements they must be implemented. What I'm saying here is that *it may be desirable* to have some protocol-based API to distinguish partial from full equivalence relations. If you accept that premise, then it is the logical consequence that if you conditionally conform `Array` to `Equatable`, you will have to implement any new APIs, and in so doing, document how equivalence of arrays of floating point values relates to floating point equivalence. For me, this is a _good thing_: it documents _in code_ something that today is muddled through.

The explanation that a method on `Float` is a "floating-point context" but a method on `[Float]` is *not a "floating point context"* is, IMO, indefensible.

Nevertheless, I will attempt to defend it :)

I find it odd that violating the documented requirements of a protocol is considered defensible, but expecting types comply with those requirements is indefensible. A principled stance would be to say that Float shouldn’t conform to Equatable (because… it doesn’t!) and requiring all calls to supply a predicate (and maybe amending types like Dictionary to allow you to supply one). That won’t fly though – users would complain – so instead we are in this murky ground.

I don't think we should defend violating the documented requirements of a protocol. Either (a) Float should not conform to Equatable (agree, this is a non-starter); (b) how Float conforms to Equatable should be brought into conformance with documented semantics (your stance); or (c) what semantics are documented should be brought into alignment with how conformance is actually implemented (my stance). Naturally, in the last case, additional APIs should be added as needed to make such reduced semantic guarantees useful for generic algorithms.

Later in the thread, you mention a possible fix for sort:

`sort()` is problematic, but not if a custom predicate is supplied.

So, we are potentially trading off one subtlety (that < behaves differently in generic and non-generic contexts) for another (that you need to know that you need to pass in a special predicate for sorting, or you get nonsense results). Knowing when an algorithm requires you to supply a predicate (like sort) vs when handling for the special case is built in (like equatable) seems far worse complication to me than knowing one rule: that generically when constrained to Comparable, Float adheres to the requirements of Comparable. Always. That is a consistent rule that you need to learn once and that doesn’t vary depending on which algorithm you’re using.

I would argue that Float should _always_ adhere to the requirements of Comparable, in all contexts. The question is, rather: what can be the requirements of Comparable such that Float can always adhere to them?

Another alternative proposed in previous threads is to give Comparable an additional operator (<=> or .compare(to:) that will always enforce a total ordering, and sort can use that. This is, afaict, C#’s solution – double.NaN < 1.0, 1.0 < double.NaN and double.NaN == double.NaN all return false, but Comparer<double>.Default.compare returns -1, 1 and 0 respectively.

This is, essentially, the endpoint of what I'm proposing.

Equatable would vend (modulo bikeshedding):
`==`, a partial equivalence relation
`~`, a full equivalence relation
`containsExceptionalValues` (yes, this is a deliberately terrible name, because it's meant to go through bikeshedding), a Boolean value to indicate whether `==` is the same as `~`

Comparable would vend (modulo bikeshedding):
`<`, `>`, <=`, `>=`, defined as now
`<=>`, as in C# `compare` (or maybe, to emphasize the point, `<~>`)
`containsExceptionalValues`, inherited from `Equatable`, to document the relationship between `<` (etc.) and the spaceship operator

This looks to me to be an absurd mess of operations, none of which will have much hope of being used in a coherent fashion by most people. Should I use == or ~ here? What are the rules again? Will people remember to not use < when they really need <=>? Probably not. Did the author of this framework I’m using remember? Dunno.

The syntax here is not the point (or if it is, it can be bikeshedded). The point I'm trying to make is that what you're criticizing as _incoherent_ is also _inescapable_. Floating-point types have a notion of equivalence that isn't full equivalence. For certain use cases (both concrete and generic), we want that partial equivalence, while for other use cases (both concrete and generic), we truly want full equivalence. To work with floating-point types correctly, a user must know that there is a difference between the two. If there is no hope of "most people" understanding this distinction when one relation is named `==` and the other is named `~`, then _a fortiori_ there is no hope of "most people" understanding the distinction when they're conflated into one operator `==` that has different behaviors in different contexts.

The C# model of compare works because < is not available generically. There is no choice between < and <=>, and so the model is simple and easily understood by both algorithm implementors and users. And if you need a different ordering, you can supply your own custom comparator. As far as I can tell, it’s a good model and users are happy with it. Swift is different, since the concrete < isexposed to the generic implementation, but having two possibilities and expecting users to pick is IMO a bad idea. Hence the proposed fix that Float’s Comparable.< is required to be a total order, per the requirements of Comparable, essentially giving us the C# model.

A true C# model would be fine, but the key point of that model to my mind is that partial equivalence and full equivalence are spelled differently (that is, `==` and `Equals`, respectively). It would not work with IEEE `==` being spelled the same way as Comparable `==`. If we were to rename the IEEE operation `&==` instead, then we'd functionally have a design that's broadly similar to the earlier version, only with different names:

Equatable would vend `==`, a full equivalence relation (and `!=`)
Comparable would vend `<`, `>`, `<=`, `>=`, now operators that reflect a total order over the set of all values; and maybe `<=>`
Floating point would additionally vend `&==` and `&<` (and `&!=`, `&<`, `&>`, `&<=`, `&>=`)

One key difference here would be that the partial equivalence relation would now only be found on floating-point types, and it would not be possible to write a generic algorithm that operates on any partially equatable or equatable type. But the other--and major--issues would be (a) that all concrete uses of floating-point comparison operators would have to be migrated to append an extra `&`; and (b) this syntax suggests that most users want to use `==` *instead of* `&==`, which I'm not sure is the case--and certainly isn't the case if they're trying to do the same things they're used to doing with floating-point values in other languages.

What about having the protocol hierarchy look like this? (All names subject to bikeshedding, of course)

protocol MaybeEquatable {
  static func ?== (lhs: Self, rhs: Self) -> Bool?
}
protocol MostlyEquatable : MaybeEquatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}
extension MostlyEquatable {
  static func ?== (lhs: Self, rhs: Self) -> Bool? { return lhs == rhs } // allows a `MostlyEquatable` or `Equatable` to function as a `MaybeEquatable` without any extra code
}
protocol Equatable : MostlyEquatable {} // purely a semantic difference, no extra syntax

protocol MaybeComparable : MaybeEquatable {
  static func ?< (lhs: Self, rhs: Self) -> Bool?
  // plus the rest of them
}
protocol MostlyComparable : MaybeComparable, MostlyEquatable {
  static func < (lhs: Self, rhs: Self) -> Bool
  // plus the rest of them
}
extension MostlyComparable {
  static func ?< (lhs: Self, rhs: Self) -> Bool? { return lhs < rhs } // allows a `MostlyComparable` or `Comparable` to function as a `MaybeComparable` without any extra code
  // plus the rest of them
}
protocol Comparable : MostlyComparable, Equatable {} // purely a semantic difference, no extra syntax

extension Double : MostlyComparable {
  static func ?== (lhs: Double, rhs: Double) -> Bool? {
    return lhs.isNaN || rhs.isNaN ? nil : lhs == rhs
  }
  static func ?< (lhs: Double, rhs: Double) -> Bool? {
    return lhs.isNaN || rhs.isNaN || (lhs.isInfinite == true && rhs.isInfinite == true && lhs.sign == rhs.sign) ? nil : lhs < rhs
  }
  static func == (lhs: Double, rhs: Double) -> Bool {
    // whatever current impl is
  }
  static func < (lhs: Double, rhs: Double) -> Bool {
    // whatever current impl is
  }
}

This would let people easily switch between the two kinds of "correct" generic comparisons (returning a `Bool?` for types that could have invalid comparisons and a `Bool` for types that can't), as well as easily opting into using IEEE-754's current "arguably incompatible with sane generic programming" behavior (by constraining T to "MostlyComparable" instead of "Comparable") if that's what they really want.

`Collection` could have different implementations of `==` depending on whether `Element` conforms to "MaybeEquatable" or "MostlyEquatable/Equatable", solving the "a = [Double.nan]; print(a == a) // prints true" issue.

This doesn't involve trapping on anything, so dunno what the performance implications would be. All the extra work would be contained in the "?*" functions, though, so at least existing code wouldn't sudden get slower.

- Dave Sweeris
_______________________________________________
swift-dev mailing list
swift-dev@swift.org <mailto:swift-dev@swift.org>
https://lists.swift.org/mailman/listinfo/swift-dev

Hi Xiaodi,

The above line is does it help your argument. Even if a person was motivated to search through the entire mailing list archives looking for said arguments, they won’t be able to guess which arguments you found to be persuasive (and who was sufficiently “core” at the time, no less). Can you please provide URLs into the archives? Is that a big ask?

Thanks,
Dave

···

On Oct 25, 2017, at 02:34, Xiaodi Wu via swift-dev <swift-dev@swift.org> wrote:

Please see earlier replies summarizing core team members' persuasive arguments as to why not multiple protocol hierarchies like this.

I can support that.

···

On Oct 20, 2017, at 10:21 AM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Oct 20, 2017 at 10:10 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Oct 20, 2017, at 9:36 AM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Fri, Oct 20, 2017 at 07:21 David Zarzycki <dave@znu.io <mailto:dave@znu.io>> wrote:

On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:
+1 for trapping unless using &==. In the case of ‘Float?’ we could also map to nil.

This is probably a more appropriate discussion for evolution though...

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it cannot be migrated to `&==` if it were felt that `==` *must* be a full equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but my opinion is that NaNs are so exceptional, and so prone to misuse, that we ought to treat them like integer arithmetic overflows: trap when they're detected, unless you use an `&` variant operator which indicates you know what you're doing.

I strongly suspect that, in practice, most float-manipulating code is not prepared to handle NaN and will not do anything sensible in its presence. For example, Apple platforms use floating-point types for geometry, color components, GPS locations, etc. Very little of this code will do anything sensible in the presence of a NaN. Arguably, it'd be better to exclude them through the type system, but I don't think that's a realistic possibility—we would need to have done that in a more source-break-friendly era. But that doesn't have to mean we're completely stuck.

Built-in floating point operators, as well as libc/libm math functions, are designed to propagate NaN correctly. This is not meant to be a thread about NaN, and we need to be cautious to define the scope of the problem to be solved from the outset. The tendency of having ever-expanding discussion where issues such as method names turn into discussions about the entire standard library go nowhere.

The question here is about `==` specifically and how to accommodate partial equivalence relations. For sanity, we start with the premise that NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve self-consistency and simplicity, then the tradeoff might be worth it. The alternative, teaching the Equality protocol about NaNs, feels like “the tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from IEEE’s opinions about language/library design.

IEEE 754 requires certain behaviors of conforming implementations and is meant to allow for portable use of floating point. Swift’s floating point facilities are conformant to that standard, and breaking IEEE 754 conformance has been repeatedly stated to be a non-starter.

The essential idea behind quiet NaN is that it can be used as input in operations that take floating point values without raising errors. Since breaking IEEE 754 conformance is a non-starter, trapping on NaN is outside the realm of available solutions.

It has been pointed out, however, that IEEE does not require a specific syntax for floating point equivalence, hence the question of whether it can be spelled differently. However, trapping on NaN is emphatically not an option.

But I really don’t want to discuss this at all here. The topic of this thread is about the semantics of Equatable.

On the other hand, changing the spelling of IEEE `==` was option in your opening post represented by demand 4 and question D.

4) IEEE-compliant floating-point equivalence must be spelled `==` and not some alternative such as `&==`.

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it cannot be migrated to `&==` if it were felt that `==` *must* be a full equivalence relation. I believe this is controversial, however.

What you call the "proximate issue” goes away if the solution to what you call the “distal issue” involves requiring `Equatable` conforming types to provide an implementation of `==` that represents a full equivalence relation. It looks to me like many people are suggesting that this is exactly what we should do.

I not a numerics expert by any means and am therefore cautious to offer my opinion, but rejecting demand 4 appears to be the best option to me. One advantage of this approach in addition to providing a full equivalence relation for floating point’s equatable conformance is that it makes it clear when an algorithm was written with IEEE semantics in mind or when the programmer did not desire or may not be aware of IEEE semantics.

I agree.

IMO, the more general issue of partial equivalence relations should be orthogonal. I don’t believe NaN should drive the semantic requirements of `Equatable` and I don’t believe IEEE should prevent floating point types from providing an Equatable conformance.

I think this is reasonable as well.

Perhaps this is a sign of my ignorance of numerics, but that’s the solution this seems to make the best tradeoffs. I am happy to let others decide whether `NaN == NaN` should trap or return `true`.

Trapping violates the requirements of Equatable and, I think, also IEEE edicts; NaN == NaN would be the only solution if we dispense with demand 4.

_______________________________________________
swift-dev mailing list
swift-dev@swift.org <mailto:swift-dev@swift.org>
https://lists.swift.org/mailman/listinfo/swift-dev

Before you guys decide anything, I would wait for Steve Canon to look at this. +CC Steve

Michael

···

On Oct 20, 2017, at 10:21 AM, Xiaodi Wu via swift-dev <swift-dev@swift.org> wrote:

On Fri, Oct 20, 2017 at 10:10 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Oct 20, 2017, at 9:36 AM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Fri, Oct 20, 2017 at 07:21 David Zarzycki <dave@znu.io <mailto:dave@znu.io>> wrote:

On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:
+1 for trapping unless using &==. In the case of ‘Float?’ we could also map to nil.

This is probably a more appropriate discussion for evolution though...

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it cannot be migrated to `&==` if it were felt that `==` *must* be a full equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but my opinion is that NaNs are so exceptional, and so prone to misuse, that we ought to treat them like integer arithmetic overflows: trap when they're detected, unless you use an `&` variant operator which indicates you know what you're doing.

I strongly suspect that, in practice, most float-manipulating code is not prepared to handle NaN and will not do anything sensible in its presence. For example, Apple platforms use floating-point types for geometry, color components, GPS locations, etc. Very little of this code will do anything sensible in the presence of a NaN. Arguably, it'd be better to exclude them through the type system, but I don't think that's a realistic possibility—we would need to have done that in a more source-break-friendly era. But that doesn't have to mean we're completely stuck.

Built-in floating point operators, as well as libc/libm math functions, are designed to propagate NaN correctly. This is not meant to be a thread about NaN, and we need to be cautious to define the scope of the problem to be solved from the outset. The tendency of having ever-expanding discussion where issues such as method names turn into discussions about the entire standard library go nowhere.

The question here is about `==` specifically and how to accommodate partial equivalence relations. For sanity, we start with the premise that NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve self-consistency and simplicity, then the tradeoff might be worth it. The alternative, teaching the Equality protocol about NaNs, feels like “the tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from IEEE’s opinions about language/library design.

IEEE 754 requires certain behaviors of conforming implementations and is meant to allow for portable use of floating point. Swift’s floating point facilities are conformant to that standard, and breaking IEEE 754 conformance has been repeatedly stated to be a non-starter.

The essential idea behind quiet NaN is that it can be used as input in operations that take floating point values without raising errors. Since breaking IEEE 754 conformance is a non-starter, trapping on NaN is outside the realm of available solutions.

It has been pointed out, however, that IEEE does not require a specific syntax for floating point equivalence, hence the question of whether it can be spelled differently. However, trapping on NaN is emphatically not an option.

But I really don’t want to discuss this at all here. The topic of this thread is about the semantics of Equatable.

On the other hand, changing the spelling of IEEE `==` was option in your opening post represented by demand 4 and question D.

4) IEEE-compliant floating-point equivalence must be spelled `==` and not some alternative such as `&==`.

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it cannot be migrated to `&==` if it were felt that `==` *must* be a full equivalence relation. I believe this is controversial, however.

What you call the "proximate issue” goes away if the solution to what you call the “distal issue” involves requiring `Equatable` conforming types to provide an implementation of `==` that represents a full equivalence relation. It looks to me like many people are suggesting that this is exactly what we should do.

I not a numerics expert by any means and am therefore cautious to offer my opinion, but rejecting demand 4 appears to be the best option to me. One advantage of this approach in addition to providing a full equivalence relation for floating point’s equatable conformance is that it makes it clear when an algorithm was written with IEEE semantics in mind or when the programmer did not desire or may not be aware of IEEE semantics.

I agree.

IMO, the more general issue of partial equivalence relations should be orthogonal. I don’t believe NaN should drive the semantic requirements of `Equatable` and I don’t believe IEEE should prevent floating point types from providing an Equatable conformance.

I think this is reasonable as well.

Perhaps this is a sign of my ignorance of numerics, but that’s the solution this seems to make the best tradeoffs. I am happy to let others decide whether `NaN == NaN` should trap or return `true`.

Trapping violates the requirements of Equatable and, I think, also IEEE edicts; NaN == NaN would be the only solution if we dispense with demand 4.

_______________________________________________
swift-dev mailing list
swift-dev@swift.org <mailto:swift-dev@swift.org>
https://lists.swift.org/mailman/listinfo/swift-dev

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

+1 for trapping unless using &==. In the case of ‘Float?’ we could also map to nil.

This is probably a more appropriate discussion for evolution though...

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it cannot be migrated to `&==` if it were felt that `==` *must* be a full equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but my opinion is that NaNs are so exceptional, and so prone to misuse, that we ought to treat them like integer arithmetic overflows: trap when they're detected, unless you use an `&` variant operator which indicates you know what you're doing.

I strongly suspect that, in practice, most float-manipulating code is not prepared to handle NaN and will not do anything sensible in its presence. For example, Apple platforms use floating-point types for geometry, color components, GPS locations, etc. Very little of this code will do anything sensible in the presence of a NaN. Arguably, it'd be better to exclude them through the type system, but I don't think that's a realistic possibility—we would need to have done that in a more source-break-friendly era. But that doesn't have to mean we're completely stuck.

Built-in floating point operators, as well as libc/libm math functions, are designed to propagate NaN correctly. This is not meant to be a thread about NaN, and we need to be cautious to define the scope of the problem to be solved from the outset. The tendency of having ever-expanding discussion where issues such as method names turn into discussions about the entire standard library go nowhere.

The question here is about `==` specifically and how to accommodate partial equivalence relations. For sanity, we start with the premise that NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve self-consistency and simplicity, then the tradeoff might be worth it. The alternative, teaching the Equality protocol about NaNs, feels like “the tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from IEEE’s opinions about language/library design.

Just to be precise: IEEE 754 places no requirements on hardware. The entirety of IEEE 754 is about what *languages* should provide. It just happens to be advantageous to implement many of the requirements directly in hardware.

[The rest of this is a response to the thread as a whole, not to Dave]

I have no philosophical objection to trapping on NaN. IEEE 754 says that the default behavior should be to not trap, but other non-default forms of exception handling* are explicitly allowed by IEEE 754.

From a practical standpoint, it’s is counter to everything about the way much floating-point hardware is designed, and that should give us some pause. On x86 it’s possible to unmask the “invalid floating point exception”, which results in any operation that generates a NaN faulting. However, it will *not* cause a fault if an operand is already a quiet NaN, so Swift would need to either test every operand that’s read from memory at the time that it’s moved into register, or test every result.

On some non-x86 architectures (including in particular most ARM implementations) there is no hardware support for unmasking exceptions, so there’s no way to automatically trap on invalid operations, you would have to explicitly check for NaN on every operation. This is much, much more expensive than checking for overflow on integer arithmetic (where for addition / subtraction, it’s just an easily-predicted conditional branch). Including these checks would introduce significant code bloat and slow down naive arithmetic by roughly an order of magnitude on current hardware, which is probably a non-starter.

Trapping only for == is much, much more palatable, but as Xiaodi said, doesn’t actually get you the semantics that you want for ==.

&== is ugly but workable. You will have inevitable bugs from people who naively adapt code from literally any other language that assumes IEEE 754 semantics for ==, however.

– Steve

[*] note that “exception handling” in an IEEE 754 context does not mean what you think it does if you’re coming from a generic CS not-floating-point background.

The performance aspect of this issue are daunting, and I'm glad you brought it up. On a cursory reading, having every NaN value compare equal to every other NaN value is likely to be several times, if not orders of magnitude, slower than the hardware IEEE implementation. This would be an extraordinary cost and makes me wonder if this is at all a good idea.

One counter argument is that &== will retain full speed where important (e.g. in a tight loop).

Off the top of my head, a naive implementation of == for Float (where Nan == Nan) would be:

  func == (a: Float, b: Float)->Bool {
    return (a &== b) || !(a &== a) || !(b &== b)
  }

The good news is that ‘a' and ‘b' only need to be loaded into registers once. Also, it could short circuit if 'a &== b' is true (if that is faster). We could probably do better with a little cleverness.

It would serve us well to re-evaluate what generic algorithms absolutely require a full equivalence relation. Let's take a look at `Array`, for example.

- `index(of:)` works perfectly sensibly without such a relation; if no NaN is equal to any other NaN, `index(of: .nan)` is appropriately `nil`.
- `min()` and `max()` are something else entirely, as we're talking here only of equivalence and not of total order, which is another issue altogether.
- `sort()` is problematic, but not if a custom predicate is supplied.
- `split()` only runs into problems if specifically trying to split a sequence on `.nan`, but again this would be unsurprising if no NaN is equal to any other NaN.
- `==` is broken but can be fixed as shown in PR #12503.

My main worry would be generic algorithms on something one level of generalization away. For example, how should a Set handle double insertion of a struct which has a Float in it. It is very likely that the creator of that struct just && together == of the elements, so the set will end up with two identical entries (neither of which would register as being a member when the set is asked).

I am currently leaning towards NaN == NaN behavior, with a warning explaining &== when used with Float == Float (and a way to silence it). That way, you at least have progressive disclosure.

Thanks,
Jon

···

On Oct 21, 2017, at 3:02 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Oct 20, 2017 at 2:42 PM, Stephen Canon <scanon@apple.com <mailto:scanon@apple.com>> wrote:

On Oct 20, 2017, at 8:21 AM, David Zarzycki via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:
On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

Please see earlier replies summarizing core team members' persuasive arguments as to why not multiple protocol hierarchies like this.

Hi Xiaodi,

The above line is does it help your argument.

Whoops. Time for more coffee… :-) "The above line does not help your argument.”

···

On Oct 25, 2017, at 08:29, David Zarzycki via swift-dev <swift-dev@swift.org> wrote:

On Oct 25, 2017, at 02:34, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

Even if a person was motivated to search through the entire mailing list archives looking for said arguments, they won’t be able to guess which arguments you found to be persuasive (and who was sufficiently “core” at the time, no less). Can you please provide URLs into the archives? Is that a big ask?

Thanks,
Dave
_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

Please see earlier replies summarizing core team members' persuasive arguments as to why not multiple protocol hierarchies like this.

Hi Xiaodi,

The above line is does it help your argument. Even if a person was motivated to search through the entire mailing list archives looking for said arguments, they won’t be able to guess which arguments you found to be persuasive (and who was sufficiently “core” at the time, no less). Can you please provide URLs into the archives? Is that a big ask?

If I'm (now) reading this correctly, he put the argument itself at the end of his earlier reply to you:

···

On Oct 25, 2017, at 5:29 AM, David Zarzycki via swift-dev <swift-dev@swift.org> wrote:

On Oct 25, 2017, at 02:34, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 24, 2017, at 7:04 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org> wrote:

On Tue, Oct 24, 2017 at 4:28 PM, David Zarzycki <dave@znu.io <mailto:dave@znu.io>> wrote:

On Oct 24, 2017, at 14:55, Ben Cohen via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

There really is no way to square this circle. Every option is going to have downsides. We have to balance correctness, least surprise/most expected behavior for most people, and consistency. For me, making generic use of Equatable and Comparable stick to the documented conformance generically, while keeping FP-specific uses the way they are, is the least bad option.

Hi Ben,

Out of curiosity, what do you think about breaking Equatable into two protocols: Equatable and Substitutable? The former would be defined in terms of mathematics, while the latter is simple to define and usable by collections, etc. For example, while +0 equals -0, they are not substitutable (because 1.0 / +0.0 == +inf BUT 1.0 / -0.0 == -inf). In the case of NaNs, substitutability would depend on the NaN payload being the same or not (because some later subsystem might interpret the NaN payload).

Finally, and unless I’m missing something, floating-point “substitutability” would translate to bitwise equality at the hardware level, which is nice from a performance perspective.

As Steve pointed out, floating-point "substitutability" as you define it is unlikely to be desired in most contexts; differentiation by NaN payload or by decimal value representation is highly unintuitive, and it would be intolerable for generic `.contains(.nan)` to sometimes return true and sometimes false depending on NaN payload.

On the topic of sorting, we could do the same thing, where we break Comparable into two protocols: Comparable and Sortable. The former would be defined in terms of mathematics, while the latter has no mathematical obligation and therefore values like +0 and -0 can be consistently sorted. The same goes of NaNs with payloads (they can be sorted). And finally, at the hardware level, this should be quite efficient because of how the floating point bits are laid out (in that an integer comparison of FP bits should work for this Sortable proposal).

Floating point values can certainly be consistently sorted as an sequence of bits, but again, it is unlikely that anyone would want such a sort of their floating point values.

But as to the topic of splitting protocols, it is essentially the design adopted by Rust. During discussion of Ben's original proposal, some core team members argued very eloquently against such a design based on what's happened in Rust. Namely:

- If Float is not Equatable but instead SomethingOtherThanEquatable (in Rust, it's called `PartialEq`), then we will have officially done our job in terms of stdlib semantic guarantees. However, in practice, people *want* to write generic algorithms that work with both integers and floating point values.
- Based on the Rust experience, what people will do when they realize that Float doesn't conform to Equatable is instead to write generic algorithms that operate on SomethingOtherThanEquatable while assuming that integer `==` and floating point `==` will work the same way.
- The result is that you now have two protocols instead of one, but most people still use only one--and still use it incorrectly. The existing problem has simply been transferred from Equatable to SomethingOtherThanEquatable. The new Equatable accomplishes nothing in terms of helping people write better code.

I just didn't catch that that was "the" argument the first time I read it.

I guess I don't understand why this is persuasive... Seems to me that even if every relevant generic function that ever ships has it's parameter conform to "SomethingOtherThanEquatable" instead of "Equatable", the extra protocol has done something: it's made the programmer acknowledge, on some level, that they might not always get a sensical result from comparing two values of that type. Neither "MostlyEquatable" nor "PartialEq" are particularly scary, so maybe they aren't the best names... If you had to constrain your generic parameters like "T: EquatableIfYouAreOKWithBridgesFallingDownAndEveryoneDying" to get the function to work with floating point types, then it'd probably get your attention.

Moreover, I'd argue that what I called "MaybeEquatable" and "MaybeComparable", where all the ops return `Bool?`, is the correct semantics for FP comparisons; it's just that most (all?) hardware doesn't support anything analogous to `Optional` (and AFAIK, neither did any programming languages at the time IEEE-754 was created), so there wasn't a way for the IEEE committee to actually express that idea.

I don't see another way out of it... IEEE floats have partial ordering/eq -- AFAICT it's inherently incorrect for them to conform to a protocol that, for all other conforming types, has total ordering/eq semantics. That said, I fully acknowledge that this is all above my pay grade (also I hadn't realized that the issue was as settled as it apparently is). If splitting the protocols is a no-go from the get go, I'll go back to trying to figure out a better way to handle it without doing that.

- Dave Sweeris

+1 for trapping unless using &==. In the case of ‘Float?’ we could also
map to nil.

This is probably a more appropriate discussion for evolution though...

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it
cannot be migrated to `&==` if it were felt that `==` *must* be a full
equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but
my opinion is that NaNs are so exceptional, and so prone to misuse, that we
ought to treat them like integer arithmetic overflows: trap when they're
detected, unless you use an `&` variant operator which indicates you know
what you're doing.

I strongly suspect that, in practice, most float-manipulating code is
not prepared to handle NaN and will not do anything sensible in its
presence. For example, Apple platforms use floating-point types for
geometry, color components, GPS locations, etc. Very little of this code
will do anything sensible in the presence of a NaN. Arguably, it'd be
better to exclude them through the type system, but I don't think that's a
realistic possibility—we would need to have done that in a more
source-break-friendly era. But that doesn't have to mean we're completely
stuck.

Built-in floating point operators, as well as libc/libm math functions,
are designed to propagate NaN correctly. This is not meant to be a thread
about NaN, and we need to be cautious to define the scope of the problem to
be solved from the outset. The tendency of having ever-expanding discussion
where issues such as method names turn into discussions about the entire
standard library go nowhere.

The question here is about `==` specifically and how to accommodate
partial equivalence relations. For sanity, we start with the premise that
NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve
self-consistency and simplicity, then the tradeoff might be worth it. The
alternative, teaching the Equality protocol about NaNs, feels like “the
tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from
IEEE’s opinions about language/library design.

Just to be precise: IEEE 754 places no requirements on hardware. The
entirety of IEEE 754 is about what *languages* should provide. It just
happens to be advantageous to implement many of the requirements directly
in hardware.

[The rest of this is a response to the thread as a whole, not to Dave]

I have no philosophical objection to trapping on NaN. IEEE 754 says that
the default behavior should be to not trap, but other non-default forms of
exception handling* are explicitly allowed by IEEE 754.

From a practical standpoint, it’s is counter to everything about the way
much floating-point hardware is designed, and that should give us some
pause. On x86 it’s possible to unmask the “invalid floating point
exception”, which results in any operation that generates a NaN faulting.
However, it will *not* cause a fault if an operand is already a quiet NaN,
so Swift would need to either test every operand that’s read from memory at
the time that it’s moved into register, or test every result.

On some non-x86 architectures (including in particular most ARM
implementations) there is no hardware support for unmasking exceptions, so
there’s no way to automatically trap on invalid operations, you would have
to explicitly check for NaN on every operation. This is much, much more
expensive than checking for overflow on integer arithmetic (where for
addition / subtraction, it’s just an easily-predicted conditional branch).
Including these checks would introduce significant code bloat and slow down
naive arithmetic by roughly an order of magnitude on current hardware,
which is probably a non-starter.

Trapping only for == is much, much more palatable, but as Xiaodi said,
doesn’t actually get you the semantics that you want for ==.

&== is ugly but workable. You will have inevitable bugs from people who
naively adapt code from literally any other language that assumes IEEE 754
semantics for ==, however.

– Steve

[*] note that “exception handling” in an IEEE 754 context does not mean
what you think it does if you’re coming from a generic CS
not-floating-point background.

The performance aspect of this issue are daunting, and I'm glad you
brought it up. On a cursory reading, having every NaN value compare equal
to every other NaN value is likely to be several times, if not orders of
magnitude, slower than the hardware IEEE implementation. This would be an
extraordinary cost and makes me wonder if this is at all a good idea.

One counter argument is that &== will retain full speed where important
(e.g. in a tight loop).

Off the top of my head, a naive implementation of == for Float (where Nan
== Nan) would be:

func == (a: Float, b: Float)->Bool {
return (a &== b) || !(a &== a) || !(b &== b)
}

That's not correct :P (Consider what happens when `a = 42` and `b = .nan`.)

The good news is that ‘a' and ‘b' only need to be loaded into registers
once. Also, it could short circuit if 'a &== b' is true (if that is
faster). We could probably do better with a little cleverness.

Steve can describe the exact number of additional machine instructions on
each architecture, but from my cursory reading the performance cost is not
good and there's little cleverness to be had.

My overarching point is that, if most generic algorithms don't break
without a full equivalence relation and others can be trivially rewritten
to accommodate floating point behavior, then incurring a non-trivial cost
by default is not a good idea.

It would serve us well to re-evaluate what generic algorithms absolutely
require a full equivalence relation. Let's take a look at `Array`, for
example.

- `index(of:)` works perfectly sensibly without such a relation; if no NaN
is equal to any other NaN, `index(of: .nan)` is appropriately `nil`.
- `min()` and `max()` are something else entirely, as we're talking here
only of equivalence and not of total order, which is another issue
altogether.
- `sort()` is problematic, but not if a custom predicate is supplied.
- `split()` only runs into problems if specifically trying to split a
sequence on `.nan`, but again this would be unsurprising if no NaN is equal
to any other NaN.
- `==` is broken but can be fixed as shown in PR #12503.

My main worry would be generic algorithms on something one level of
generalization away. For example, how should a Set handle double insertion
of a struct which has a Float in it. It is very likely that the creator of
that struct just && together == of the elements, so the set will end up
with two identical entries (neither of which would register as being a
member when the set is asked).

Consider a comparison of two instances of Complex<Float>: it should
absolutely return `false` when evaluating `Complex(1.0, .nan) ==
Complex(1.0, .nan)`. Likewise, if (actually, let's put it another way: as
long as) NaN != NaN, then an instance of Set<Float> should not deduplicate
NaN, and NaN should never be a member of that Set, and neither should an
instance of Set<Complex<Float>>. That's _correct_ in my view.

The wonkiness with `Array.==` is one of the few places where the behavior
of the generic algorithm is inexplicable on the basis of what's publicly
explained to the user. By contrast, `index(of:)`, `split`, etc. all behave
predictably given the fact that NaN != NaN.

I am currently leaning towards NaN == NaN behavior, with a warning

···

On Sat, Oct 21, 2017 at 7:52 PM, Jonathan Hull <jhull@gbis.com> wrote:

On Oct 21, 2017, at 3:02 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Oct 20, 2017 at 2:42 PM, Stephen Canon <scanon@apple.com> wrote:

On Oct 20, 2017, at 8:21 AM, David Zarzycki via swift-dev < >> swift-dev@swift.org> wrote:
On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org> >> wrote:
On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com> wrote:

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev < >>> swift-dev@swift.org> wrote:
On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev < >>> swift-dev@swift.org> wrote:

explaining &== when used with Float == Float (and a way to silence it).
That way, you at least have progressive disclosure.

Right, to which I replied that I wasn’t proposing the Rust model or your similar “MaybeEquatable” model. I was proposing something different where Float and Int are both Equatable and Substitutable, but neither Equatable nor Substitutable inherit from the other. The former is mathematical and the latter is not (which allows it to deal with NaN payloads, ±0, etc). Generic algorithms care mostly if not completely about mathematics, while generic containers care mostly if not completely about substitutability. They can live alongside each other and get along peacefully/sanely. And if people need to care about both, then at least they have an out.

Dave

···

On Oct 25, 2017, at 12:01, David Sweeris <davesweeris@mac.com> wrote:

On Oct 25, 2017, at 5:29 AM, David Zarzycki via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 25, 2017, at 02:34, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

Please see earlier replies summarizing core team members' persuasive arguments as to why not multiple protocol hierarchies like this.

Hi Xiaodi,

The above line is does it help your argument. Even if a person was motivated to search through the entire mailing list archives looking for said arguments, they won’t be able to guess which arguments you found to be persuasive (and who was sufficiently “core” at the time, no less). Can you please provide URLs into the archives? Is that a big ask?

If I'm (now) reading this correctly, he put the argument itself at the end of his earlier reply to you:

I don’t think it is settled. The issue that Xiaodi mentioned was a PartiallyEq protocol which still had a signature of (T,T)->Bool. People just used that protocol instead of Equatable without taking into account the difference in behavior. The signature of (T,T)->Bool? changes things because people are forced to deal with the optional.

Currently, I think we should do 3 things:

1) Create a new protocol with a partial equivalence relation with signature of (T, T)->Bool? and automatically conform Equatable things to it
2) Depreciate Float, etc’s… Equatable conformance with a warning that it will eventually be removed (and conform Float, etc… to the partial equivalence protocol)
3) Provide an '&==‘ relation on Float, etc… (without a protocol) with the native Float IEEE comparison

I think this provides several benefits. #3 allows pure speed when needed, but not in a generic context (and is appropriately scary to cause some thought). #1 forces correct handling in generic contexts. #2 gives people time to make the adjustment, but also eventually requires them to switch to using #1 or #3.

I think it will cause A LOT of currently incorrect code to be fixed.

Thanks,
Jon

···

On Oct 25, 2017, at 9:01 AM, David Sweeris via swift-dev <swift-dev@swift.org> wrote:

That said, I fully acknowledge that this is all above my pay grade (also I hadn't realized that the issue was as settled as it apparently is). If splitting the protocols is a no-go from the get go, I'll go back to trying to figure out a better way to handle it without doing that.

+1 for trapping unless using &==. In the case of ‘Float?’ we could also map to nil.

This is probably a more appropriate discussion for evolution though...

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it cannot be migrated to `&==` if it were felt that `==` *must* be a full equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday, but my opinion is that NaNs are so exceptional, and so prone to misuse, that we ought to treat them like integer arithmetic overflows: trap when they're detected, unless you use an `&` variant operator which indicates you know what you're doing.

I strongly suspect that, in practice, most float-manipulating code is not prepared to handle NaN and will not do anything sensible in its presence. For example, Apple platforms use floating-point types for geometry, color components, GPS locations, etc. Very little of this code will do anything sensible in the presence of a NaN. Arguably, it'd be better to exclude them through the type system, but I don't think that's a realistic possibility—we would need to have done that in a more source-break-friendly era. But that doesn't have to mean we're completely stuck.

Built-in floating point operators, as well as libc/libm math functions, are designed to propagate NaN correctly. This is not meant to be a thread about NaN, and we need to be cautious to define the scope of the problem to be solved from the outset. The tendency of having ever-expanding discussion where issues such as method names turn into discussions about the entire standard library go nowhere.

The question here is about `==` specifically and how to accommodate partial equivalence relations. For sanity, we start with the premise that NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve self-consistency and simplicity, then the tradeoff might be worth it. The alternative, teaching the Equality protocol about NaNs, feels like “the tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable from IEEE’s opinions about language/library design.

Just to be precise: IEEE 754 places no requirements on hardware. The entirety of IEEE 754 is about what *languages* should provide. It just happens to be advantageous to implement many of the requirements directly in hardware.

[The rest of this is a response to the thread as a whole, not to Dave]

I have no philosophical objection to trapping on NaN. IEEE 754 says that the default behavior should be to not trap, but other non-default forms of exception handling* are explicitly allowed by IEEE 754.

From a practical standpoint, it’s is counter to everything about the way much floating-point hardware is designed, and that should give us some pause. On x86 it’s possible to unmask the “invalid floating point exception”, which results in any operation that generates a NaN faulting. However, it will *not* cause a fault if an operand is already a quiet NaN, so Swift would need to either test every operand that’s read from memory at the time that it’s moved into register, or test every result.

On some non-x86 architectures (including in particular most ARM implementations) there is no hardware support for unmasking exceptions, so there’s no way to automatically trap on invalid operations, you would have to explicitly check for NaN on every operation. This is much, much more expensive than checking for overflow on integer arithmetic (where for addition / subtraction, it’s just an easily-predicted conditional branch). Including these checks would introduce significant code bloat and slow down naive arithmetic by roughly an order of magnitude on current hardware, which is probably a non-starter.

Trapping only for == is much, much more palatable, but as Xiaodi said, doesn’t actually get you the semantics that you want for ==.

&== is ugly but workable. You will have inevitable bugs from people who naively adapt code from literally any other language that assumes IEEE 754 semantics for ==, however.

– Steve

[*] note that “exception handling” in an IEEE 754 context does not mean what you think it does if you’re coming from a generic CS not-floating-point background.

The performance aspect of this issue are daunting, and I'm glad you brought it up. On a cursory reading, having every NaN value compare equal to every other NaN value is likely to be several times, if not orders of magnitude, slower than the hardware IEEE implementation. This would be an extraordinary cost and makes me wonder if this is at all a good idea.

One counter argument is that &== will retain full speed where important (e.g. in a tight loop).

Off the top of my head, a naive implementation of == for Float (where Nan == Nan) would be:

  func == (a: Float, b: Float)->Bool {
    return (a &== b) || !(a &== a) || !(b &== b)
  }

That's not correct :P (Consider what happens when `a = 42` and `b = .nan`.)

Oh, you are right. I have accidentally made .nan equal to everything instead of nothing. That will teach me to write code in mail without sleep.

That should have been (also in mail without sleep… I have learned nothing!):

  func == (a: Float, b: Float)->Bool {
    return (a &== b) || !( a &== a || b &== b )
  }

The good news is that ‘a' and ‘b' only need to be loaded into registers once. Also, it could short circuit if 'a &== b' is true (if that is faster). We could probably do better with a little cleverness.

Steve can describe the exact number of additional machine instructions on each architecture, but from my cursory reading the performance cost is not good and there's little cleverness to be had.

He also said that for only == it was "much, much more palatable"

My overarching point is that, if most generic algorithms don't break without a full equivalence relation and others can be trivially rewritten to accommodate floating point behavior, then incurring a non-trivial cost by default is not a good idea.

I am ok incurring cost (especially when it is recoverable) if it will help avoid pitfalls. I guess the question is: how many pitfalls are there from breaking reflexivity?

It is one of those things that you hear veteran programmers constantly warning about, which tells me that they have been bitten a few times.

It would serve us well to re-evaluate what generic algorithms absolutely require a full equivalence relation. Let's take a look at `Array`, for example.

- `index(of:)` works perfectly sensibly without such a relation; if no NaN is equal to any other NaN, `index(of: .nan)` is appropriately `nil`.
- `min()` and `max()` are something else entirely, as we're talking here only of equivalence and not of total order, which is another issue altogether.
- `sort()` is problematic, but not if a custom predicate is supplied.
- `split()` only runs into problems if specifically trying to split a sequence on `.nan`, but again this would be unsurprising if no NaN is equal to any other NaN.
- `==` is broken but can be fixed as shown in PR #12503.

My main worry would be generic algorithms on something one level of generalization away. For example, how should a Set handle double insertion of a struct which has a Float in it. It is very likely that the creator of that struct just && together == of the elements, so the set will end up with two identical entries (neither of which would register as being a member when the set is asked).

Consider a comparison of two instances of Complex<Float>: it should absolutely return `false` when evaluating `Complex(1.0, .nan) == Complex(1.0, .nan)`. Likewise, if (actually, let's put it another way: as long as) NaN != NaN, then an instance of Set<Float> should not deduplicate NaN, and NaN should never be a member of that Set, and neither should an instance of Set<Complex<Float>>. That's _correct_ in my view.

Right, in the context of Float, but my point was that the issues ripple outward.

Part of the issue is that we have generic algorithms which have an expectation of reflexivity from ==. But we also need to consider generic algorithms which build on expectations of things like Set, where the expected behavior has been changed because of how == behaves regarding NaN. As you said, you consider duplication of NaN in Sets the correct behavior, so that means that anything building on Set generically will need to consider this aberrant behavior in the same way they need to consider ==. It ripples outward, and each level gets harder to plan for.

I can’t write generic code which puts something in a collection and then expects that thing to be a member of the collection (this could even lead to infinite loops). The count of a collection and the number of members I can test for are potentially different. The number of resulting special cases which one has to keep in mind grows exponentially as it ripples outwards.

Also, you can’t just special case Float, because it is anything containing a Float.

The wonkiness with `Array.==` is one of the few places where the behavior of the generic algorithm is inexplicable on the basis of what's publicly explained to the user. By contrast, `index(of:)`, `split`, etc. all behave predictably given the fact that NaN != NaN.

One of the places we have found so far. This is a notorious area for bugs to happen though. It is not just the standard library. I’ll bet that people have a bunch of bugs in generic code they have written involving dictionaries with keys containing a Float somewhere.

How does the PR handle Array<Complex> == Array<Complex>? Wouldn’t it have the same issue?

I am currently leaning towards NaN == NaN behavior, with a warning explaining &== when used with Float == Float (and a way to silence it). That way, you at least have progressive disclosure.

Thanks,
Jon

···

On Oct 21, 2017, at 6:27 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Sat, Oct 21, 2017 at 7:52 PM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:

On Oct 21, 2017, at 3:02 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:
On Fri, Oct 20, 2017 at 2:42 PM, Stephen Canon <scanon@apple.com <mailto:scanon@apple.com>> wrote:

On Oct 20, 2017, at 8:21 AM, David Zarzycki via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:
On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:

+1 for trapping unless using &==. In the case of ‘Float?’ we could
also map to nil.

This is probably a more appropriate discussion for evolution though...

D) Must floating-point IEEE-compliant equivalence be spelled `==`?

In my view, this is something open for debate. I see no reason why it
cannot be migrated to `&==` if it were felt that `==` *must* be a full
equivalence relation. I believe this is controversial, however.

I actually got partway through writing up a pitch on this yesterday,
but my opinion is that NaNs are so exceptional, and so prone to misuse,
that we ought to treat them like integer arithmetic overflows: trap when
they're detected, unless you use an `&` variant operator which indicates
you know what you're doing.

I strongly suspect that, in practice, most float-manipulating code is
not prepared to handle NaN and will not do anything sensible in its
presence. For example, Apple platforms use floating-point types for
geometry, color components, GPS locations, etc. Very little of this code
will do anything sensible in the presence of a NaN. Arguably, it'd be
better to exclude them through the type system, but I don't think that's a
realistic possibility—we would need to have done that in a more
source-break-friendly era. But that doesn't have to mean we're completely
stuck.

Built-in floating point operators, as well as libc/libm math functions,
are designed to propagate NaN correctly. This is not meant to be a thread
about NaN, and we need to be cautious to define the scope of the problem to
be solved from the outset. The tendency of having ever-expanding discussion
where issues such as method names turn into discussions about the entire
standard library go nowhere.

The question here is about `==` specifically and how to accommodate
partial equivalence relations. For sanity, we start with the premise that
NaN will forever be as it is.

I support Jonathan’s argument. If Swift wants to trap on NaN to improve
self-consistency and simplicity, then the tradeoff might be worth it. The
alternative, teaching the Equality protocol about NaNs, feels like “the
tail wagging the dog".

In short: what IEEE requires of floating-point hardware is separable
from IEEE’s opinions about language/library design.

Just to be precise: IEEE 754 places no requirements on hardware. The
entirety of IEEE 754 is about what *languages* should provide. It just
happens to be advantageous to implement many of the requirements directly
in hardware.

[The rest of this is a response to the thread as a whole, not to Dave]

I have no philosophical objection to trapping on NaN. IEEE 754 says that
the default behavior should be to not trap, but other non-default forms of
exception handling* are explicitly allowed by IEEE 754.

From a practical standpoint, it’s is counter to everything about the way
much floating-point hardware is designed, and that should give us some
pause. On x86 it’s possible to unmask the “invalid floating point
exception”, which results in any operation that generates a NaN faulting.
However, it will *not* cause a fault if an operand is already a quiet NaN,
so Swift would need to either test every operand that’s read from memory at
the time that it’s moved into register, or test every result.

On some non-x86 architectures (including in particular most ARM
implementations) there is no hardware support for unmasking exceptions, so
there’s no way to automatically trap on invalid operations, you would have
to explicitly check for NaN on every operation. This is much, much more
expensive than checking for overflow on integer arithmetic (where for
addition / subtraction, it’s just an easily-predicted conditional branch).
Including these checks would introduce significant code bloat and slow down
naive arithmetic by roughly an order of magnitude on current hardware,
which is probably a non-starter.

Trapping only for == is much, much more palatable, but as Xiaodi said,
doesn’t actually get you the semantics that you want for ==.

&== is ugly but workable. You will have inevitable bugs from people who
naively adapt code from literally any other language that assumes IEEE 754
semantics for ==, however.

– Steve

[*] note that “exception handling” in an IEEE 754 context does not mean
what you think it does if you’re coming from a generic CS
not-floating-point background.

The performance aspect of this issue are daunting, and I'm glad you
brought it up. On a cursory reading, having every NaN value compare equal
to every other NaN value is likely to be several times, if not orders of
magnitude, slower than the hardware IEEE implementation. This would be an
extraordinary cost and makes me wonder if this is at all a good idea.

One counter argument is that &== will retain full speed where important
(e.g. in a tight loop).

Off the top of my head, a naive implementation of == for Float (where Nan
== Nan) would be:

func == (a: Float, b: Float)->Bool {
return (a &== b) || !(a &== a) || !(b &== b)
}

That's not correct :P (Consider what happens when `a = 42` and `b = .nan`.)

Oh, you are right. I have accidentally made .nan equal to everything
instead of nothing. That will teach me to write code in mail without sleep.

That should have been (also in mail without sleep… I have learned
nothing!):

func == (a: Float, b: Float)->Bool {
return (a &== b) || !( a &== a || b &== b )
}

The good news is that ‘a' and ‘b' only need to be loaded into registers

once. Also, it could short circuit if 'a &== b' is true (if that is
faster). We could probably do better with a little cleverness.

Steve can describe the exact number of additional machine instructions on
each architecture, but from my cursory reading the performance cost is not
good and there's little cleverness to be had.

He also said that for only == it was "much, much more palatable"

...which it is, in the grand scheme of things, as only a tiny proportion of
your code would be `==`. But `==` itself will still perform abysmally, and
a method such as `elementsEqual` that recursively evaluates `==` would be
very, very sad.

My overarching point is that, if most generic algorithms don't break

without a full equivalence relation and others can be trivially rewritten
to accommodate floating point behavior, then incurring a non-trivial cost
by default is not a good idea.

I am ok incurring cost (especially when it is recoverable) if it will help
avoid pitfalls. I guess the question is: how many pitfalls are there from
breaking reflexivity?

It is one of those things that you hear veteran programmers constantly
warning about, which tells me that they have been bitten a few times.

To be clear, the most viable alternative to abandoning demand #4, to my
mind, is abandoning demand #1--that is, explicitly clarifying that
Equatable only guarantees a partial equivalence relation.

It would then be necessary to offer additional methods to determine if the
implementation's `==` is in fact a full equivalence relation. This is what
PR #12503 does with underscored methods. If this course of action is
settled upon, those methods would eventually become full public API with
improved names after Evolution. With conditional conformances, `Sequence
where Element : Equatable` would implement `public static var
_hasExceptionalValues : Bool { return Element._hasExceptionalValues }`.

Fundamentally, there is no magic to be had here: it's all tradeoffs.

If we make NaN == NaN, then *every* use of floating point `==` must incur a
performance cost or be migrated. Practically speaking, the average
developer would be conditioned to type `&==` when working with
floating-point code, therefore avoiding no more pitfalls as compared to the
status quo when working with floating point. However, any custom generic
code *might* be more robust--unless they rely on total ordering of
Comparable methods, which will still not be the case ((1 < NaN) == false
*and* (NaN < 1) == false), or assume seemingly "obvious" properties of
Numeric, which will still not be the case (remember, a + b - b != a if
you're working with floating-point values thanks to rounding). Put it this
way: very few generic algorithms that are incorrect now _would be correct
but for the fact that NaN != NaN_; accommodating floating-point types is
much trickier than just that. (I was the one who fixed accumulated rounding
error in floating point strides back in the day.)

On the other hand, if we formalize `==` as a *partial* equivalence
relation, then *every* use of `==` in correct generic code must either rely
only on partial equivalence or explicitly check for full equivalence.
Practically speaking, the average developer will carry on as they always
have and their generic code will have the same pitfalls as they do today;
they won't test for correctness using floating-point values, and if they
try to use floating-point values as input, the algorithm may fail to yield
the expected result--but just as likely because of the myriad other
pitfalls of floating-point types that have nothing to do with NaN != NaN.
However, any custom concrete floating point code will remain performant,
and any uses of stdlib methods will return predictable results because we
will fix them.

From a pragmatic standpoint, a user is *astronomically* more commonly going

to use concrete floating point `==` than write generic algorithms on
collection types which happen only rely on equivalence and not any other
facilities for which floating-point types are uniquely wonky, then feed
that algorithm floating-point values.

It would serve us well to re-evaluate what generic algorithms absolutely

require a full equivalence relation. Let's take a look at `Array`, for
example.

- `index(of:)` works perfectly sensibly without such a relation; if no
NaN is equal to any other NaN, `index(of: .nan)` is appropriately `nil`.
- `min()` and `max()` are something else entirely, as we're talking here
only of equivalence and not of total order, which is another issue
altogether.
- `sort()` is problematic, but not if a custom predicate is supplied.
- `split()` only runs into problems if specifically trying to split a
sequence on `.nan`, but again this would be unsurprising if no NaN is equal
to any other NaN.
- `==` is broken but can be fixed as shown in PR #12503.

My main worry would be generic algorithms on something one level of
generalization away. For example, how should a Set handle double insertion
of a struct which has a Float in it. It is very likely that the creator of
that struct just && together == of the elements, so the set will end up
with two identical entries (neither of which would register as being a
member when the set is asked).

Consider a comparison of two instances of Complex<Float>: it should
absolutely return `false` when evaluating `Complex(1.0, .nan) ==
Complex(1.0, .nan)`. Likewise, if (actually, let's put it another way: as
long as) NaN != NaN, then an instance of Set<Float> should not deduplicate
NaN, and NaN should never be a member of that Set, and neither should an
instance of Set<Complex<Float>>. That's _correct_ in my view.

Right, in the context of Float, but my point was that the issues ripple
outward.

Part of the issue is that we have generic algorithms which have an
expectation of reflexivity from ==. But we also need to consider generic
algorithms which build on expectations of things like Set, where the
expected behavior has been changed because of how == behaves regarding
NaN. As you said, you consider duplication of NaN in Sets the correct
behavior, so that means that anything building on Set generically will need
to consider this aberrant behavior in the same way they need to consider
==. It ripples outward, and each level gets harder to plan for.

It only "ripples outward" in the sense that formalizing `==` as a partial
equivalence relation requires each use of `==` in the generic setting to
test for full equivalence if necessary. But that is what it means to make
or not to make a semantic guarantee. Moreover, as a consequence of the
present design where implementation doesn't match documentation, it's
*already necessary* to do so in today's Swift if you want your algorithms
to work with floating point. Finally, the reality of the situation is that
many algorithms will require such planning in order to accommodate floating
point even if NaN == NaN.

I can’t write generic code which puts something in a collection and then

expects that thing to be a member of the collection (this could even lead
to infinite loops). The count of a collection and the number of members I
can test for are potentially different.

Right, but again, even today, you can't rely on these properties in
reality. On a practical level, consider how often you write generic code
that deals only with equivalence of elements in collections (and not
ordering, or arithmetic, or any other protocol-based method).

The number of resulting special cases which one has to keep in mind grows

exponentially as it ripples outwards.

Also, you can’t just special case Float, because it is anything containing
a Float.

The wonkiness with `Array.==` is one of the few places where the behavior
of the generic algorithm is inexplicable on the basis of what's publicly
explained to the user. By contrast, `index(of:)`, `split`, etc. all behave
predictably given the fact that NaN != NaN.

One of the places we have found so far.

It's not a mystery where these problems arise. I've personally raised the
issue with `Array.==` a few times over the last two years. The entire
standard library can be made compatible with partial equivalence `==` with
trivial migration.

This is a notorious area for bugs to happen though. It is not just the
standard library. I’ll bet that people have a bunch of bugs in generic code
they have written involving dictionaries with keys containing a Float
somewhere.

For many, many reasons that do not at all involve NaN, I do believe the
common advice is *not* to use floating-point keys. You find the same advice
for C++, C#, Python, and on and on...

How does the PR handle Array<Complex> == Array<Complex>? Wouldn’t it have

the same issue?

Complex would have to make use of the underscored APIs; again, if this plan
of action is adopted, then the additional APIs would go through Evolution
and `==` as partial equivalence would become formalized.

I am currently leaning towards NaN == NaN behavior, with a warning

···

On Sat, Oct 21, 2017 at 10:16 PM, Jonathan Hull <jhull@gbis.com> wrote:

On Oct 21, 2017, at 6:27 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Sat, Oct 21, 2017 at 7:52 PM, Jonathan Hull <jhull@gbis.com> wrote:

On Oct 21, 2017, at 3:02 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Oct 20, 2017 at 2:42 PM, Stephen Canon <scanon@apple.com> wrote:

On Oct 20, 2017, at 8:21 AM, David Zarzycki via swift-dev < >>> swift-dev@swift.org> wrote:
On Oct 20, 2017, at 07:51, Xiaodi Wu via swift-dev <swift-dev@swift.org> >>> wrote:
On Fri, Oct 20, 2017 at 1:22 AM, Jonathan Hull <jhull@gbis.com> wrote:

On Oct 19, 2017, at 9:48 PM, Brent Royal-Gordon via swift-dev < >>>> swift-dev@swift.org> wrote:
On Oct 19, 2017, at 4:29 PM, Xiaodi Wu via swift-dev < >>>> swift-dev@swift.org> wrote:

explaining &== when used with Float == Float (and a way to silence it).
That way, you at least have progressive disclosure.

Thanks,
Jon

Steve can describe the exact number of additional machine instructions on each architecture, but from my cursory reading the performance cost is not good and there's little cleverness to be had.

I'm not sure why you think there's no cleverness to be had. For instance, suppose we loosen our requirement a bit: We require the `==` operator to be reflexive, but we allow `BinaryFloatingPoint.==` to treat NaNs with different payloads as different values. That very nearly allows us to use integer equality for floating-point comparison; we only need a +0/-0 special case:

  extension Double {
    private var bits: Int {
      return unsafeBitCast(self, to: Int64.self)
    }
    
    static func == (lhs: Double, rhs: Double) -> Bool {
      let lhsBits = lhs.bits
      let rhsBits = rhs.bits
      let nonSignBits = ~((-0 as Double).bits)
      
      return lhsBits == rhsBits || ((lhsBits | rhsBits) & nonSignBits) == 0
    }
  }

`BinaryFloatingPoint.<` requires more branching, but it doesn't look too bad either:

  extension Double {
    static func < (lhs: Double, rhs: Double) -> Bool {
      let lhsBits = lhs.bits
      let rhsBits = rhs.bits
      
      let signBits = (-0 as Double).bits
      let nonSignBits = ~signBits

      if (lhsBits & signBits) == (rhsBits & signBits) {
        // lhs < rhs (where signs are the same) is equivalent to integer comparison.
        return lhsBits < rhsBits
      }
      else {
        // lhs < rhs (where signs are different) if lhs is negative and they aren't both zero.
        return (lhsBits & signBits) == 0 && ((lhsBits | rhsBits) & nonSignBits) != 0
      }
    }
  }

(I could be wrong about these; I'm not a floating-point expert.)
      
These implementations would make sorting and searching work *mostly* sensibly; the only problem would be that, for instance, `numbers.contains(.nan)` might return false if there's a NaN in `numbers` with a different payload than the one `.nan` returns. Personally, I think that's an acceptable cost.

My overarching point is that, if most generic algorithms don't break without a full equivalence relation and others can be trivially rewritten to accommodate floating point behavior, then incurring a non-trivial cost by default is not a good idea.

Yes, but the "if" is doing a lot of work here—and I don't think it's actually true. Things in the standard library that currently don't accommodate floating-point behavior include:

• Our `sort()`, `min()`, and `max()` methods
• Our `Set` and `Dictionary` types

Can these be trivially rewritten? I'm not sure about most of them, but I *can* come up with a trivial rewrite for `min()`:

  public func min() -> Element? {
    var it = makeIterator()
    guard var result = it.next() else { return nil }
    for e in IteratorSequence(it) {
      if !(result == result) || e < result { result = e }
    }
    return result
  }

But that brings us to the second question: Do we really expect random people to rewrite their code like this? I mean, we're literally doing a reflexivity check in this code. Is that a sensible thing to force on our users?

(For that matter, I'm not sure I could rewrite `min(by:)` this way if we treat the comparator as equivalent to a `<`. That seems like a sign we're Doing It Wrong.)

···

On Oct 21, 2017, at 6:27 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org> wrote:

--
Brent Royal-Gordon
Architechies

Please see earlier replies summarizing core team members' persuasive
arguments as to why not multiple protocol hierarchies like this.

Hi Xiaodi,

The above line is does it help your argument. Even if a person was
motivated to search through the entire mailing list archives looking for
said arguments, they won’t be able to guess which arguments you found to be
persuasive (and who was sufficiently “core” at the time, no less). Can you
please provide URLs into the archives? Is that a big ask?

If I'm (now) reading this correctly, he put the argument itself at the end
of his earlier reply to you:

Right, to which I replied that I wasn’t proposing the Rust model or your
similar “MaybeEquatable” model.

The other Dave was proposing such a thing, and that was my reply to him.
You're right that you are proposing something different.

I was proposing something different where Float and Int are both Equatable
and Substitutable, but neither Equatable nor Substitutable inherit from the
other. The former is mathematical and the latter is not (which allows it to
deal with NaN payloads, ±0, etc). Generic algorithms care mostly if not
completely about mathematics, while generic containers care mostly if not
completely about substitutability. They can live alongside each other and
get along peacefully/sanely. And if people need to care about both, then at
least they have an out.

The issue with this is similar to that in my reply earlier about bitwise
comparison of floating-point values. Yes, you can propose some total
ordering over floating-point values totally divorced from `==`, but I'm
quite certain that people who invoke `sort()` on an array of floating-point
values don't simply want *some* deterministic order, but rather an actual
increasing order of numeric values. Likewise, when someone asks if an array
contains a floating-point value (say, `10.0 as Decimal`), they generally
want to know if *any* representation of that value exists. The point is
that _what kind of substitutability_ matters, and the kind that people will
expect for floating-point values is the very mathematical substitutability
that is supposed to be guaranteed by Equatable, which simply does not
accommodate NaN.

···

On Wed, Oct 25, 2017 at 12:06 PM, David Zarzycki <dave@znu.io> wrote:

On Oct 25, 2017, at 12:01, David Sweeris <davesweeris@mac.com> wrote:
On Oct 25, 2017, at 5:29 AM, David Zarzycki via swift-dev < > swift-dev@swift.org> wrote:
On Oct 25, 2017, at 02:34, Xiaodi Wu via swift-dev <swift-dev@swift.org> > wrote:

One issue which occurred to me only recently, which I hadn't considered,
renders my `&==` idea and all similar schemes untenable:

Useful algorithms can and are written which operate on both floating-point
and integer numeric types. In fact, the whole point of laboriously
designing `Numeric` as part of SE-0104 was to make it possible to do so. If
IEEE comparison is relegated to `FloatingPoint` and the only operator
remaining on `Numeric` is `==`, then not only will there be a mandatory
performance hit, but currently correct algorithms can be broken with
absolutely no way to express a fix.

···

On Wed, Oct 25, 2017 at 8:26 PM, Jonathan Hull <jhull@gbis.com> wrote:

> On Oct 25, 2017, at 9:01 AM, David Sweeris via swift-dev < > swift-dev@swift.org> wrote:
>
> That said, I fully acknowledge that this is all above my pay grade (also
I hadn't realized that the issue was as settled as it apparently is). If
splitting the protocols is a no-go from the get go, I'll go back to trying
to figure out a better way to handle it without doing that.

I don’t think it is settled. The issue that Xiaodi mentioned was a
PartiallyEq protocol which still had a signature of (T,T)->Bool. People
just used that protocol instead of Equatable without taking into account
the difference in behavior. The signature of (T,T)->Bool? changes things
because people are forced to deal with the optional.

Currently, I think we should do 3 things:

1) Create a new protocol with a partial equivalence relation with
signature of (T, T)->Bool? and automatically conform Equatable things to it
2) Depreciate Float, etc’s… Equatable conformance with a warning that it
will eventually be removed (and conform Float, etc… to the partial
equivalence protocol)
3) Provide an '&==‘ relation on Float, etc… (without a protocol) with the
native Float IEEE comparison

I think this provides several benefits. #3 allows pure speed when needed,
but not in a generic context (and is appropriately scary to cause some
thought). #1 forces correct handling in generic contexts. #2 gives people
time to make the adjustment, but also eventually requires them to switch to
using #1 or #3.

I think it will cause A LOT of currently incorrect code to be fixed.

This way leads to madness: a set S that contains a NaN, but S.contains(T.nan) is false. You’re going to run into worse problems when you get to Decimal; is 1e2 == 100e0? They have totally different encodings.

If we’re going to define another notion of floating-point equality in order to satisfy the usual mathematical axioms, it should treat all NaNs as equal, and all encodings of any non-zero value as equal. Whether it treats +0 and -0 as the same or different is the only really question from my perspective (I come down *very* weakly on the side of “they’re different”).

– Steve

···

On Oct 23, 2017, at 1:47 AM, Brent Royal-Gordon via swift-dev <swift-dev@swift.org> wrote:

On Oct 21, 2017, at 6:27 PM, Xiaodi Wu via swift-dev <swift-dev@swift.org> wrote:

Steve can describe the exact number of additional machine instructions on each architecture, but from my cursory reading the performance cost is not good and there's little cleverness to be had.

I'm not sure why you think there's no cleverness to be had. For instance, suppose we loosen our requirement a bit: We require the `==` operator to be reflexive, but we allow `BinaryFloatingPoint.==` to treat NaNs with different payloads as different values. That very nearly allows us to use integer equality for floating-point comparison; we only need a +0/-0 special case:

>
> Steve can describe the exact number of additional machine instructions
on each architecture, but from my cursory reading the performance cost is
not good and there's little cleverness to be had.

I'm not sure why you think there's no cleverness to be had. For instance,
suppose we loosen our requirement a bit: We require the `==` operator to be
reflexive, but we allow `BinaryFloatingPoint.==` to treat NaNs with
different payloads as different values. That very nearly allows us to use
integer equality for floating-point comparison; we only need a +0/-0
special case:

        extension Double {
                private var bits: Int {
                        return unsafeBitCast(self, to: Int64.self)
                }

                static func == (lhs: Double, rhs: Double) -> Bool {
                        let lhsBits = lhs.bits
                        let rhsBits = rhs.bits
                        let nonSignBits = ~((-0 as Double).bits)

                        return lhsBits == rhsBits || ((lhsBits | rhsBits)
& nonSignBits) == 0
                }
        }

Besides all of Steve's points (which are absolutely key to consider), this
function is clearly more expensive than a single machine instruction.
Consider also how much more expensive it would be in the case where this
custom equality prevents SIMD parallelization.

`BinaryFloatingPoint.<` requires more branching, but it doesn't look too

bad either:

        extension Double {
                static func < (lhs: Double, rhs: Double) -> Bool {
                        let lhsBits = lhs.bits
                        let rhsBits = rhs.bits

                        let signBits = (-0 as Double).bits
                        let nonSignBits = ~signBits

                        if (lhsBits & signBits) == (rhsBits & signBits) {
                                // lhs < rhs (where signs are the same) is
equivalent to integer comparison.
                                return lhsBits < rhsBits
                        }
                        else {
                                // lhs < rhs (where signs are different)
if lhs is negative and they aren't both zero.
                                return (lhsBits & signBits) == 0 &&
((lhsBits | rhsBits) & nonSignBits) != 0
                        }
                }
        }

(I could be wrong about these; I'm not a floating-point expert.)

These implementations would make sorting and searching work *mostly*
sensibly; the only problem would be that, for instance,
`numbers.contains(.nan)` might return false if there's a NaN in `numbers`
with a different payload than the one `.nan` returns. Personally, I think
that's an acceptable cost.

> My overarching point is that, if most generic algorithms don't break
without a full equivalence relation and others can be trivially rewritten
to accommodate floating point behavior, then incurring a non-trivial cost
by default is not a good idea.

Yes, but the "if" is doing a lot of work here—and I don't think it's
actually true. Things in the standard library that currently don't
accommodate floating-point behavior include:

• Our `sort()`, `min()`, and `max()` methods
• Our `Set` and `Dictionary` types

Again, `Set` and `Dictionary` accommodate floating-point types just
fine--in that their behavior is consistent with NaN != NaN. That is, if you
accept that NaN != NaN, then there is nothing to fix about the
implementation of `Set` or `Dictionary`.

As to `sort`, `min`, and `max`, again, those would not be addressed by
reflexive equality of NaN; that would require a total ordering over
floating point values, which is another issue entirely (and, also, exists
as `FloatingPoint.isTotallyOrdered(belowOrEqualTo:)`, a function with
behavior dictated by the IEEE standard). We are discussing here only
whether NaN == NaN should be required by Equatable; NaN would still be
unordered relative to all other values for the purposes of `<` and other
comparison operators.

Can these be trivially rewritten? I'm not sure about most of them, but I

*can* come up with a trivial rewrite for `min()`:

  public func min() -> Element? {
    var it = makeIterator()
    guard var result = it.next() else { return nil }
    for e in IteratorSequence(it) {
      if !(result == result) || e < result { result = e }
    }
    return result
  }

You don't need to rewrite from scratch; what you want to use is
`FloatingPoint.minimum(_:_:)` (again, a function with behavior dictated by
the IEEE standard).

But that brings us to the second question: Do we really expect random

people to rewrite their code like this? I mean, we're literally doing a
reflexivity check in this code. Is that a sensible thing to force on our
users?

Floating point is hard; we cannot hide the complexity of floating point
merely by redefining `==`. We do, however, stand a good chance of making
things worse by deviating from the common practice in other languages.

···

On Mon, Oct 23, 2017 at 12:47 AM, Brent Royal-Gordon <brent@architechies.com > wrote:

> On Oct 21, 2017, at 6:27 PM, Xiaodi Wu via swift-dev < > swift-dev@swift.org> wrote:

(For that matter, I'm not sure I could rewrite `min(by:)` this way if we
treat the comparator as equivalent to a `<`. That seems like a sign we're
Doing It Wrong.)

--
Brent Royal-Gordon
Architechies

As someone mentioned earlier, we are trying to square a circle here. We can’t have everything at once… we will have to prioritize. I feel like the precedent in Swift is to prioritize safety/correctness with an option ignore safety and regain speed.

I think the 3 point solution I proposed is a good compromise that follows that precedent. It does mean that there is, by default, a small performance hit for floats in generic contexts, but in exchange for that, we get increased correctness and safety. This is the exact same tradeoff that Swift makes for optionals! Any speed lost can be regained by providing a specific override for FloatingPoint that uses ‘&==‘.

For example, if someone wants to write a generic function that works both on Integer and FloatingPoint, then they would have to use the new protocol which would force them to correctly handle cases involving NaN. If speed is super important in that particular case, then they can write overrides for the FloatingPoint case which uses &==, and for Equatable which uses ==.

Because Float’s Equatable conformance is just being depreciated (with a warning/fixit), authors have at least a version to decide whether speed or correctness (or hopefully both) is most important to them.

Thanks,
Jon

P.S. We really should not be comparing against the speed of algorithms which don’t correctly handle NaN. Let’s compare Apples to Apples.

···

On Oct 25, 2017, at 6:36 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Wed, Oct 25, 2017 at 8:26 PM, Jonathan Hull <jhull@gbis.com <mailto:jhull@gbis.com>> wrote:
> On Oct 25, 2017, at 9:01 AM, David Sweeris via swift-dev <swift-dev@swift.org <mailto:swift-dev@swift.org>> wrote:
>
> That said, I fully acknowledge that this is all above my pay grade (also I hadn't realized that the issue was as settled as it apparently is). If splitting the protocols is a no-go from the get go, I'll go back to trying to figure out a better way to handle it without doing that.

I don’t think it is settled. The issue that Xiaodi mentioned was a PartiallyEq protocol which still had a signature of (T,T)->Bool. People just used that protocol instead of Equatable without taking into account the difference in behavior. The signature of (T,T)->Bool? changes things because people are forced to deal with the optional.

Currently, I think we should do 3 things:

1) Create a new protocol with a partial equivalence relation with signature of (T, T)->Bool? and automatically conform Equatable things to it
2) Depreciate Float, etc’s… Equatable conformance with a warning that it will eventually be removed (and conform Float, etc… to the partial equivalence protocol)
3) Provide an '&==‘ relation on Float, etc… (without a protocol) with the native Float IEEE comparison

I think this provides several benefits. #3 allows pure speed when needed, but not in a generic context (and is appropriately scary to cause some thought). #1 forces correct handling in generic contexts. #2 gives people time to make the adjustment, but also eventually requires them to switch to using #1 or #3.

I think it will cause A LOT of currently incorrect code to be fixed.

One issue which occurred to me only recently, which I hadn't considered, renders my `&==` idea and all similar schemes untenable:

Useful algorithms can and are written which operate on both floating-point and integer numeric types. In fact, the whole point of laboriously designing `Numeric` as part of SE-0104 was to make it possible to do so. If IEEE comparison is relegated to `FloatingPoint` and the only operator remaining on `Numeric` is `==`, then not only will there be a mandatory performance hit, but currently correct algorithms can be broken with absolutely no way to express a fix.