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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With