[swift-dev] Rationalizing FloatingPoint conformance to Equatable

Xiaodi Wu xiaodi.wu at gmail.com
Thu Oct 26 23:00:02 CDT 2017

On Thu, Oct 26, 2017 at 4:57 PM, Greg Parker <gparker at apple.com> wrote:

> On Oct 26, 2017, at 11:47 AM, Xiaodi Wu via swift-dev <swift-dev at swift.org>
> wrote:
> On Thu, Oct 26, 2017 at 1:30 PM, Jonathan Hull <jhull at gbis.com> wrote:
>> Now you are just being rude. We all want Swift to be awesome… let’s try
>> to keep things civil.
> Sorry if my reply came across that way! That wasn't at all the intention.
> I really mean to ask you those questions and am interested in the answers:
> Unless I misunderstand, you're arguing that your proposal is superior to
> Rust's design because of a new operator that returns `Bool?` instead of
> `Bool`; if so, how is it that you haven't reproduced Rust's design problem,
> only with the additional syntax involved in unwrapping the result?
> And if, as I understand, your argument is that your design is superior to
> Rust's *because* it requires unwrapping, then isn't the extent to which
> people will avoid using the protocol unintentionally also equally and
> unavoidably the same extent to which it makes Numeric more cumbersome?
> You said it was impossible, so I gave you a very quick example showing
>> that the current behavior was still possible.  I wasn’t recommending that
>> everyone should only ever use that example for all things.
>> For FloatingPoint, ‘(a &== b) == true’ would mimic the current behavior
>> (bugs and all). It may not hold for all types.
> No, the question was how it would be possible to have these guarantees
> hold for `Numeric`, not merely for `FloatingPoint`, as the purpose is to
> use `Numeric` for generic algorithms. This requires additional semantic
> guarantees on what you propose to call `&==`.
> Would something like this work?
> Numeric.== -> Bool
> traps on NaN etc.

This is unsatisfactory for several reasons:

- If it is not tolerable for NaN to trap when doing math with
floating-point values (and the very notion of "quiet NaN" is predicated on
that insight), then it cannot be tolerable for NaN to trap in generic
numeric code.

- As the whole raison d'être of `Numeric` is to permit useful generic
numeric algorithms, `Numeric.==` must offer the best practicable
approximation of mathematical equality for any conforming type. On a
concrete numeric type, it would be exceedingly user-hostile if `==` did not
represent the best practicable approximation of mathematical equality for
that type. Therefore, `Numeric.==` must be the same operator as
`FloatingPoint.==` and `Integer.==`. Despite necessary differences between
floating-point and integer values, these two concrete operators are spelled
the same way because they are both the best practicable approximations of
mathematical equality for the numeric values that their respective types
attempt to model (see below). If `Numeric.==` does not offer the closest
approximations of mathematical equality available for conforming types,
there is little point to offering `Numeric` as a generic protocol.

Numeric.==? -> Bool?
> returns nil on NaN etc. You likely don't want this unless you know
> something about floating-point.
> Numeric.&== -> Bool
> is IEEE equality. You should not use this unless you are a floating-point
> expert.

I think we are proceeding from different starting points here.

It would be contrary to all sense to have a method named `Int.==` be
anything other than the best practicable approximation of mathematical
equality for `Int`. The same holds for floating-point types.

Either the IEEE definition of floating-point equality is the best such
approximation, or it is not. If it is not, then IEEE equality should not be
spelled `==` on any type or in any context. But, having weighed all the
alternatives, a committee of floating-point experts has blessed this
definition over others. As I understand it, this definition treats the
sequence of bits as the real number it attempts to represent to the
greatest extent possible, abstracting away encoding and representation
issues, and it excludes from the relation all NaNs because they are not in
the domain of real numbers.

So my starting point, then, is that (based on IEEE expertise) there is one
and only one proper definition of `==` for floating-point types, and that
it is the IEEE definition. You *should* use this definition in all places
to test for whether two floating-point values are equal. And Swift *should*
present IEEE equality as *the* go-to operator for equivalence of
floating-point values (which the core team has already declared on this
list to represent the uncountable set of real numbers and not the finite
set of representable numbers).

A proper design for `Equatable` and `Comparable` would accommodate
floating-point types while also making it possible to write generic
algorithms that behave correctly. It should be a non-goal to make
floating-point `==` anything other than what it is (i.e., IEEE-compliant).
Nor is it necessary (or, perhaps, even desirable) to eliminate
consideration of NaN from generic code. The only goal here (or at least, my
only goal here) is to ensure that writing generic code that uses `==` which
behaves properly with NaN is no more difficult than writing
floating-point-specific code that uses `==` which behaves properly with NaN.

The experts can get high performance or sophisticated numeric behavior. The
> rest of us who naïvely use == get a relatively foolproof floating-point
> model. (There is no difference among these three operators for fixed-size
> integers, of course.)
> This is analogous to what Swift does with integer overflow. I would
> further argue the other Numeric operators like + should be extended to the
> same triple of trap or optional or just-do-it. We already have two of those
> three operators for integer addition after all.
> Numeric.+ -> T
> traps on FP NaN and integer overflow

Again, `Numeric.+` is and must be the same as `FloatingPoint.+` and
`Integer.+`. They are spelled the same way because they are both the best
practicable approximations of mathematical addition for the numeric values
that their respective types attempt to model. Wraparound is an inferior
approximation of mathematical addition, for example, because its semantics
take into consideration the underlying representation of the integral value
as a fixed-length series of bits.

> Numeric.+? -> T?
> returns nil on FP NaN and integer overflow
> Numeric.&+ -> T
> performs FP IEEE addition and integer wraparound

These two operations have entirely distinct semantics. No useful generic
algorithm could be written that uses this operator correctly.

> The whole point is that you have to put thought into how you want to deal
>> with the optional case where the relation’s guarantees have failed.
>> If you need full performance, then you would have separate overrides on
>> Numeric for members which conform to FloatingPoint (where you could use
>> &==) and Equatable (where you could use ==). As you get more generic, you
>> lose opportunities for optimization. That is just the nature of generic
>> code. The nice thing about Swift is that you have an opportunity to
>> specialize if you want to optimize more. Once things like conditional
>> conformances come online, all of this will be nicer, of course.
> This is a non-starter then. Protocols must enable useful generic code.
> What you're basically saying is that you do not intend for it to be
> possible to use methods on `Numeric` to ask about level 1 equivalence in a
> way that would not be prohibitively expensive. This, again, eviscerates the
> purpose of `Numeric`.
> I'm not sure that there is a performance problem. If your compiled code is
> actually making calls to generic comparison functions then you have already
> lost the high performance war. Any place where the compiler knows enough to
> use a specialized comparison function should also be a place where the
> compiler can optimize away unnecessary floating-point checks.
> Let me make an analogous objection to the current Numerics design. How do
> you get the highest performance addition operator in a generic context?
> Currently you can't, because Numeric.+ checks for integer overflow.
> The point I'm making here, again, is that there are legitimate uses for
> `==` guaranteeing partial equivalence in the generic context. The
> approximation being put forward over and over is that generic code always
> requires full equivalence and concrete floating-point code always requires
> IEEE partial equivalence. That is _not true_. Some generic code (for
> instance, that which uses `Numeric`) relies on partial equivalence
> semantics and some floating-point code can nonetheless benefit from a
> notion of full equivalence.
> I agree that providing a way to get IEEE equality in a generic context is
> useful. I am not convinced that Numeric.== -> Bool is the right place to
> provide it.
> --
> Greg Parker     gparker at apple.com     Runtime Wrangler
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-dev/attachments/20171026/913d8ec0/attachment.html>

More information about the swift-dev mailing list