Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type inference fails when using nil-coalescing operator with two optionals

We are trying to figure whether this is a bug in Swift or us misusing generics, optionals, type inference and/or nil coalescing operator.

Our framework contains some code for parsing dictionaries into models and we've hit a problem with optional properties with default values.

We have a protocol SomeProtocol and two generic functions defined in a protocol extension:

mapped<T>(...) -> T?
mapped<T : SomeProtocol>(...) -> T?

Our structs and classes adhere to this protocol and then parse their properties inside an init function required by the protocol.

Inside the init(...) function we try to set a value of the property someNumber like this:

someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber

The dictionary of course contains the actual value for key someNumber. However, this will always fail and the actual value will never get returned from the mapped() function.

Either commenting out the second generic function or force downcasting the value on the rhs of the assignment will fix this issue, but we think this should work the way it currently is written.


Below is a complete code snippet demonstrating the issue, along with two options that (temporarily) fix the issue labeled OPTION 1 and OPTION 2 in the code:

import Foundation

// Some protocol

protocol SomeProtocol {
    init(dictionary: NSDictionary?)
}

extension SomeProtocol {
    func mapped<T>(dictionary: NSDictionary?, key: String) -> T? {
        guard let dictionary = dictionary else {
            return nil
        }

        let source = dictionary[key]
        switch source {

        case is T:
            return source as? T

        default:
            break
        }

        return nil
    }

    // ---
    // OPTION 1: Commenting out this makes it work
    // ---

    func mapped<T where T:SomeProtocol>(dictionary: NSDictionary?, key: String) -> T? {
        return nil
    }
}

// Some struct

struct SomeStruct {
    var someNumber: Double? = 0.0
}

extension SomeStruct: SomeProtocol {
    init(dictionary: NSDictionary?) {
        someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber

        // OPTION 2: Writing this makes it work
        // someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber!
    }
}

// Test code

let test = SomeStruct(dictionary: NSDictionary(object: 1234.4567, forKey: "someNumber"))
if test.someNumber == 1234.4567 {
    print("success \(test.someNumber!)")
} else {
    print("failure \(test.someNumber)")
}

Please note, that this is an example which misses the actual implementations of the mapped functions, but the outcome is identical and for the sake of this question the code should be sufficient.


EDIT: I had reported this issue a while back and now it was marked as fixed, so hopefully this shouldn't happen anymore in Swift 3.
https://bugs.swift.org/browse/SR-574

like image 971
Dominik Hadl Avatar asked Jan 18 '16 14:01

Dominik Hadl


1 Answers

You've given the compiler too many options, and it's picking the wrong one (at least not the one you wanted). The problem is that every T can be trivially elevated to T?, including T? (elevated to T??).

someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber

Wow. Such types. So Optional. :D

So how does Swift begin to figure this thing out. Well, someNumber is Double?, so it tries to turn this into:

Double? = Double?? ?? Double?

Does that work? Let's look for a generic mapped, starting at the most specific.

func mapped<T where T:SomeProtocol>(dictionary: NSDictionary?, key: String) -> T? {

To make this work, T has to be Double?. Is Double?:SomeProtocol? Nope. Moving on.

func mapped<T>(dictionary: NSDictionary?, key: String) -> T? {

Does this work? Sure! T can be Double? We return Double?? and everything resolves.

So why does this one work?

someNumber = self.mapped(dictionary, key: "someNumber") ?? someNumber!

This resolves to:

Double? = Optional(Double? ?? Double)

And then things work the way you think they're supposed to.

Be careful with so many Optionals. Does someNumber really have to be Optional? Should any of these things throw? (I'm not suggesting throw is a general work-around for Optional problems, but at least this problem gives you a moment to consider if this is really an error condition.)

It is almost always a bad idea to type-parameterize exclusively on the return value in Swift the way mapped does. This tends to be a real mess in Swift (or any generic language that has lots of type inference, but it really blows up in Swift when there are Optionals involved). Type parameters should generally appear in the arguments. You'll see the problem if you try something like:

let x = test.mapped(...)

It won't be able to infer the type of x. This isn't an anti-pattern, and sometimes the hassle is worth it (and in fairness, the problem you're solving may be one of those cases), but avoid it if you can.

But it's the Optionals that are killing you.


EDIT: Dominik asks a very good question about why this behaves differently when the constrained version of mapped is removed. I don't know. Obviously the type matching engine checks for valid types in a little different order depending on how many ways mapped is generic. You can see this by adding print(T.self) to mapped<T>. That might be considered a bug in the compiler.

like image 135
Rob Napier Avatar answered Nov 14 '22 14:11

Rob Napier