[swift-evolution] [pitch] Comparison Reform

Jonathan Hull jhull at gbis.com
Tue Apr 25 22:28:36 CDT 2017


> On Apr 25, 2017, at 7:17 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
> 
> On Tue, Apr 25, 2017 at 8:38 PM, Jonathan Hull <jhull at gbis.com <mailto:jhull at gbis.com>> wrote:
> 
>> On Apr 25, 2017, at 5:25 PM, Xiaodi Wu <xiaodi.wu at gmail.com <mailto:xiaodi.wu at gmail.com>> wrote:
>> 
>> On Tue, Apr 25, 2017 at 6:53 PM, Jonathan Hull <jhull at gbis.com <mailto:jhull at gbis.com>> wrote:
>> I just wanted to ask for more detail on why this is a non-starter (it seems like most of my ideas are dismissed as “non-starters”, but I am rarely given a detailed reason why).
>> 
>> Migration would be a renaming from ‘Double' to ‘Double?’, but it wouldn’t be cosmetic.  It would free us to use a non-optional Double, where we can guarantee the answer wouldn’t be NaN/nil.  We would, as you say, have functions like ‘cos(Double?)->Double?’ which propagate the optional, but we could also add a ‘cos(Double)->Double’ overload which guarantees an actual result.  For something like Tan, we would only have the optional version because the answer may actually be undefined, even when the input isn't.
>> 
>> Leave aside how one might implement such a system for a moment. The first major issue is that your idea does not address the issues we're talking about here:
>> 
>> We are debating, for instance, how to compare arrays with elements of type `Double`. In your design, the question remains how we would compare arrays with elements of type `Double?`. If the answer is that you cannot compare arrays of type `[Double?]`, then we have a problem, because that's the type that people will use when they ingest data that might contain NaN. Sure, they can unwrap each element before using their data, but they can also test each element with `isNaN` today. We are trying to *improve* on the user experience of comparing arrays of floating point values *without* checking if they contain NaN, not trying to take that feature away.
> 
> It solves the main problem we are dealing with in Comparable because defining Comparable on non-optional Double (without possibility of NaN) is fairly simple.
> 
> Yes, but `Double` would not the currency type for floating point work. Since any input might contain NaN, people will be working pervasively with `Double?`. Some consideration as to how the type would have to be designed (I will leave that as an exercise to the reader) would reveal that `Double` would either have trapping arithmetic operators or no arithmetic operators at all. Who would use such a type? And for what purpose? No, you've just pushed the problem from Double to Double?, but the issue is unsolved, and now we have two types where before we had one.

Any value of type ‘Double’ would be guaranteed not to contain NaN.  Some things coming in from ObjC land might use ‘Double?', but that is only a single ‘guard let' statement away from ‘Double’. Once you have a Double, you would in most cases get a Double back out. In the event that you do receive a ‘Double?’, you know that you actually do have to deal with the optional case (either through chaining or bailing early in the case of nil).  It enforces proper behavior which would likely be implemented in a buggy way under the current system.

You would probably have trapping arithmetic for divide by zero, just like Int.  The other option is to return an optional, but you and Dave have previously made strong arguments for trapping.

 
> You could compare an array of ‘Double?' in the same way you could compare an array of ‘Int?’ now.  It would work the same way optionals work everywhere else.
> 
> Right, but again, `Double?` would be the currency type for floating point work. And you have still not told me how arrays of `Double?` elements would work. Would two arrays of NaN compare equal? Would two NaNs compare equal? If NaN == NaN, then we've got a problem. The starting point for this discussion is that, when working with floating point types, NaN != NaN. It's required by IEEE standards, and it's relied upon by users.

‘Double?' would only be the currency type for interfacing with external (non-swift) code.  Most swift code would still just use Double… and when it requires ‘Double?’, you know you need to deal with the possibility of nil.  Right now, NaN is just ignored for the most part… even in some standard library code.


> You would just test for a lack of numerical result by checking the optional in the usual Swift way: guard let, if let, etc...
> 
> 
>> Your design also doesn't address the problem of how NaN should compare with NaN. Only now, you've pushed the problem to `Optional`. By design, every `Optional<T>.none` compares equal to every other `Optional<U>.none` (yes, even of different types).
> 
> Yes, the one major difference in behavior, which I mentioned in my earlier email, is that we are replacing NaN != NaN with nil == nil.  Everything else should behave the same.  The part that saves us here, is that Swift forces you to explicitly consider the optional case.
> 
> Basically, I am taking the spirit/intent of the law over the letter of it.  As far as I can tell, (quiet) NaN was originally designed to provide the functionality which we use optional for in Swift.  The NaN != NaN thing came out of limitations of the computers in the 1980’s because you needed a way to tell if something was NaN or not (and isNaN() didn’t exist yet).
> 
> I'll refer you to Steve Canon's answer on StackOverflow: http://stackoverflow.com/questions/1565164/what-is-the-rationale-for-all-comparisons-returning-false-for-ieee754-nan-values/1573715#1573715 <http://stackoverflow.com/questions/1565164/what-is-the-rationale-for-all-comparisons-returning-false-for-ieee754-nan-values/1573715#1573715>
I get what he is saying about not being able to suddenly change C’s definition to the opposite boolean value (even if they would have designed it differently for modern systems).  We aren’t C though.  And nil is a different name than NaN.  This gives us a way to make a change without breaking conformance.


> Bottom line is, whatever it's designed for, it's here to stay. I'm *not* on any IEEE committee; I'm not qualified to design an alternative universe of floating point types; and the Swift evolution mailing list (or even Swift itself) is not the place to design one. I and others rely on Swift floating point types to be IEEE-complaint, so that's where we start the discussion here about Comparable.

It *is* IEEE compliant (or at least the same amount we currently are), what is different is the way we interface with that in code.  You can think of Swift Double as a simple (runtime-cost-free) wrapper around the compiler’s double that limits how you can touch it (in order to provide additional safety guarantees).  Sounds very swifty to me… we do similar things with other C constructs using various wrappers.  We aren’t getting rid of NaN behind the scenes, just packaging it’s use in a way which aligns with Swift as a whole…  What changes is the programer’s mental model. The bits are exactly the same.

If the naming is what bothers you, we could even just create a “new” Swift type that is this wrapper, and then discourage the use of Double outside of places where NaN is needed.  I feel like NaN is actually needed in relatively few places though…

>  
> The programmer (mental) model would be that Swift Double just doesn’t have NaN, and anywhere where you would normally return NaN, you return nil instead.
> 
> Look, you can already use Double? today. There is no barrier to trying it out for yourself. However, `Double?.none` doesn't mean the same thing as `Double.nan`. The former indicates that _there is no value_; the latter indicates that there _is_ a value, it's just _not a number_. Suppose I parse a list of numbers into an array. If I ask for [Double].last and get nil, it's telling me that _there are no elements in the array_. If I get .nan, it's telling me that there *is* a value, it just wasn't a number. In the former case, I'd do nothing. In the latter case, I might prompt the user: this isn't a number!
>  
> However, the property of using NaN’s bits to represent nil let’s us inter-op seamlessly with C and ObjC (or any other language’s) code.  They just treat it as a double with NaN as normal (including NaN != NaN) and we interface with it as ‘Double?'
> 
> I'm going to sound like a broken record, now. Whether floating point types in Swift conform to IEEE standards is _not_ up for discussion, afaict; that's simply a given. Now, around that constraint, we're trying to design a revised Comparable protocol. Code written today for floating point work expects NaN != NaN. That is just something that is and will forever be. We break source compatibility if we change that.

As I said above, they *do* conform to those standards… they just don’t expose the full capabilities directly to the end programmer.  The capabilities are still there, you just have to be more explicit about their use.  

I could be wrong, but I believe that in the current version of Swift, the result of doing comparisons with NaN is actually undefined at the moment… so it isn’t breaking source compatibility with the defined language.  Just with code which is using implementation artifacts...

C code might require some simple changes to work in Swift, but as you argued passionately before, that is already to be expected.  We shouldn’t have the expectation of copy/pasting C code without thinking through the differences in &+, etc...

> 
>> This did not always work correctly, if I recall, but it does now and it's an intentional part of the design. However, NaN must compare not equal to every NaN. These could not be more different properties. It seems quite absurd on its face that we might want NaN to compare equal to a value of type `Optional<UIViewController>`.
> 
> Is there an algorithm that requires NaN != NaN that couldn’t be reasonably rewritten to handle nil/optionals instead? 
> 
> I don't need an algorithm to show you the problem. See this expression: `0 * Double.infinity == .infinity * 0` correctly evaluates to false. Zero times infinity is simply not equal to infinity times zero. You're suggesting a design where the result would be true, and that simply won't fly, because it's just not true.

IEEE 754 says that the result of any comparison with NaN should be *undefined*.  C’s implementation is the one who brings us this idea that NaN != NaN.  The *correct* evaluation according to 754 would be ‘undefined’.  We aren’t breaking 754, just breaking away from a C convention… in a way which is very much in line with how Swift breaks away from other C conventions.

I still haven’t heard any actual working use cases where this property is actually important other than in legacy C code.

> 
> I haven’t been able to think of one. They are extremely similar, because they were designed for the same use-cases.  The examples you have given so far (e.g. cos(Double)->Double) would all be trivial to migrate.
> 
> Thanks,
> Jon
> 
> 
>> 
>> 
>> In short, it would actually make people consider conditions which result in NaN because of Swift’s machinery which makes people consider nil.
>> 
>> It also allows non-optional Double to easily conform to Comparable, and removes the gotchas around Collections…  Pretty big wins for a “cosmetic rename”.  The only thing we lose is 'NaN != Nan' (because nil == nil), but then algorithms that had relied on that would be forced to consider the NaN/nil case explicitly because of the optionals.
>> 
>> It would also inter-op well with C/ObjC code by just having the compiler overlay Double? for Double…
>> 
>> Thanks,
>> Jon
>> 
>> 
>>> On Apr 16, 2017, at 11:44 AM, Xiaodi Wu <xiaodi.wu at gmail.com <mailto:xiaodi.wu at gmail.com>> wrote:
>>> 
>>> On Sun, Apr 16, 2017 at 1:14 PM, Jonathan Hull <jhull at gbis.com <mailto:jhull at gbis.com>> wrote:
>>> 
>>>> On Apr 16, 2017, at 10:42 AM, Xiaodi Wu via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>> 
>>>> The point is that, when you manipulate two real numbers, sometimes there is no numeric result. You cannot simply wish this away with a new numeric type because it is not an artifact of _modeling_ real numbers but rather intrinsic to mathematics itself.
>>> 
>>> I agree with the rest of what you said, but I have to disagree on this point.  What I think he is saying is that, in Swift, we really should be representing the NaN case as an optional instead of a magic value on the type itself (similar to how swift uses an optional instead of NSNotFound).
>>> 
>>> In fact, that might be an actual option here.  For ‘Double?’ the compiler could use the bit pattern for NaN internally to represent .none (I believe it does similar tricks to save space with other optional types).  Then disallow reference to NaN within swift code.  Functions or operations which could produce NaN would either have to produce an optional or trap in case of NaN. (e.g. the trig functions would likely return an optional, and 0/0 would trap).
>>> 
>>> I think it would actually lead to much better code because the compiler would force you to have to explicitly deal with the case of optional/NaN when it is possible.  Migration would be tricky though...
>>> 
>>> This is essentially a cosmetic renaming from `Double` to `Double?`. There are rules for propagating NaN which numeric algorithms expect. For example, `cos(.nan)` returns a value. If your design is to work, every function that takes a `Double` will need to take a `Double?`.
>>> 
>>> Just as Swift String conforms to Unicode standards, FloatingPoint conforms to IEEE standards. You'd have to come up with enormous benefits to justify breaking that. Doing so for Swift 4 is plainly a non-starter.
>>> 
>>> Thanks,
>>> Jon

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170425/c7cb73ce/attachment.html>


More information about the swift-evolution mailing list