Unexpected behavior when using `flatMap` to transform `Double?` to `Float`

foundation
bug
swift

(Maz Jaleel) #1

I'm seeing some extremely weird behavior in my macOS project, which I am easily reproducing in the REPL.

Basically, Float(double) outputs different results than double.flatMap(Float.init). I understand there must be a different init overload invoked when using flatMap, but why? Looks like a bug in the Swift compiler to me, but I'm not sure, probably there is something I'm missing.

Any help on shedding more light on the matter is much appreciated.

  1> let d: Double? = 0.4
d: Double? = 0.40000000000000002
  2> d.flatMap(Float.init)
$R0: Float? = nil
  3> Float(d!)
$R1: Float = 0.400000006


Edit: Indeed, another overload was triggered, based on the detailed explanation given by @lukasa

  4> d.flatMap(Float.init(_:))
$R2: Float? = 0.400000006

(Cory Benfield) #2

You can solve this from the signature of Optional<Double>.flatMap:

func flatMap<U>(_ transform: (Double) throws -> U?) rethrows -> U?

The transform function must take a non-optional Double and return an optional U. In this case, we're using a Float initialiser, so it must return a Float?. Float has loads of initialisers, but here are the ones that take Double:

init(_ other: Double)
init?(exactly: Double)

Notice that only one of these returns an Optional<Float>: init?(exactly:). This initializer is the one being invoked in the FlatMap context. When you invoke the Float initializer in the next line, you aren't providing a label, so you cannot possibly be invoking init?(exactly:): you must be invoking init(_ value:), which will convert to the nearest representation.

In this case you probably don't want flatMap, so if you change your flatMap line to map you'll get the behaviour you want.


(Adrian Zubarev) #3

That's still is a little strange. Why does type inference ignore the missing label in this case. Shouldn't it emit an error that there is no Float.init that return a Float? instead? So if you really want to use flatMap it will require you to manually change it to flatMap(Float.init(exactly:)). At first glance I rather thought that it was implicit optional promotion that wrapped Float.init(_: Double) into a Float?.


(Cory Benfield) #4

Generally speaking Swift does not require the presence of labels in cases where a function reference is being passed by name. See:

func test(x: Int) -> Int {
    return x + 1
}

func takesBlock(_ block: (Int) -> Int) -> Int {
    return block(1)
}

takesBlock(test)

I cannot speak to the design reason for that, but I can theorise. Here are my thoughts in no particular order:

  1. It is not reasonable to allow functions that accept closures to constrain the labels on those closures, so the closure type syntax may not contain labels (that is, you may not write func t(_ block: (x: Int) -> Int)).

  2. Given that, it is clear that the labels must be ignored when invoking a block: as we cannot constrain their names, we don't know what they are when invoking the block.

  3. Given that, it is unreasonable for functions that expect labels to have to have those labels called out when invoking the block. Can you imagine the frustration in having to provide the full label-including function name in every case where you passed a function that normally has argument labels to a closure parameter?

  4. Given that function labels are not required to pass the function to the block, the absence of them in the source probably shouldn't be used to weight the choice of which of a similar set of functions to call. This can be observed in Swift as well, by amending the above code to:

    func test(x: Int) -> Int {
        return x + 1
    }
    
    func test(_ x: Int) -> Int {
        return x + 2
    }
    
    func takesBlock(_ block: (Int) -> Int) -> Int {
        return block(1)
    }
    
    takesBlock(test)
    

    which fails to compile, complaining (rightfully) about the ambiguous use of test. This is despite the fact that in all other usages there would be no ambiguity about the use of test, as we either would or would not provide the label.

The TL;DR here is that labels simply don't play into the selection process of closure arguments unless you explicitly choose to provide them. If you do, then they can help the compiler by constraining the valid choices of the compiler. The absence of them, however, cannot be a similar constraint without being profoundly annoying in the most-common case.


(Jordan Rose) #5

We've talked about requiring the labels when you pass the function as a value. I don't think it's completely out of the question as a language change. Another variation would be to require the labels only when there are overloads, or only when there are overloads with different labels.

I certainly recommend using the labels whenever there are overloads, precisely because of things like this.


(Cory Benfield) #6

This seems like the most reasonable compromise. Requiring it always would be a hilariously breaking language change, as I have almost never seen the pattern of referring to a method including its labels (except in my own code, sometimes, for purely aesthetic reasons).


#7

I had to do it once, when I added an overload to init that only differed in the label name. It actually took me a little while to understand why code which compiled a few minutes ago was suddenly broken, as far as the compiler was concerned.