Type inference error when using the nil-coalescing operator with two options

We are trying to figure out if this is a mistake in Swift or we are abusing generics, options, a type operator and / or a nil-coalescing operator.

Our structure contains code for parsing dictionaries in the model, and we ran into a problem with additional properties with default values.

We have the SomeProtocol protocol and two common functions defined in the protocol extension:

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

Our structures and classes adhere to this protocol, and then analyze their properties inside the init function required by the protocol.

Inside the init(...) function, we are trying to set the value of the someNumber property as follows:

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

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

Either commenting on the second generic function, or the force lowering the value in the rhs assignment, will fix this problem, but we believe that this should work as it is currently written.


Below is the full code snippet that demonstrates the problem, as well as two options that (temporarily) fix the problem with the 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)") } 

Note that this is an example that skips the actual implementations of mapped functions, but the result is identical, and the code should be sufficient for this question.


EDIT . I reported this issue a while ago and it has now been flagged as fixed, so hopefully this should no longer happen in Swift 3.
https://bugs.swift.org/browse/SR-574

+7
generics swift optional
source share
1 answer

You gave the compiler too many options, and it chose the wrong one (at least not the one you need). The problem is that every T can be trivially upgraded to T? including T? (upgraded to T?? ).

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

Wow. These types. So, optional .: D

So, as Swift begins to understand this thing. Well, someNumber is Double? so he is trying to turn this into:

 Double? = Double?? ?? Double? 

It works? Let's look at general mapped , starting with the most specific.

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

To do this job, T must be Double? . Is Double?:SomeProtocol ? Nope. We move on.

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

It works? Of course! T maybe Double? We return Double?? and everything is allowed.

So why does this work?

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

This permits:

 Double? = Optional(Double? ?? Double) 

And then everything works the way you think they should.

Be careful with so many options. Could someNumber be optional? If any of these things throw ? (I do not suggest throw be a general workaround for additional tasks, but at least this problem gives you the opportunity to think about whether this is really an error condition.)

It is almost always a bad idea to enter parameterization solely on the return value in Swift using the mapped method. This is usually a real mess in Swift (or any generic language that has many types of output, but it really explodes in Swift when there are options associated with it). Type parameters should usually be displayed in arguments. You will see a problem if you try something like:

 let x = test.mapped(...) 

He will not be able to infer type x . This is not an anti-pattern, and sometimes it is a hassle (and, frankly, the problem you are solving may be one of those cases), but avoid it if you can.

But these are the Options that are killing you.


EDIT: Dominic asks a very good question why it behaves differently when the remote version of mapped removed. I dont know. Obviously, the type matching mechanism checks for valid types in a slightly different order, depending on how many mapped methods are common. You can see this by adding print(T.self) to the mapped<T> . This may be considered a compiler error.

+7
source share

All Articles