Confusing behavior of type inference with generics and optionals - not sure if bug or expected

I recently did some minor refactoring in a code base involving a initialization of a property on a custom view that was causing some issues when it was loading up in interface builder. The change I did works, but it's a bit confusing about why it does, and if it's expected behavior or not. Note that for the below I'm using Xcode 9.2/Swift 4.0.

There's a dependency injection resolver defined as:

func resolve<T>() -> T? {
  //...
}

and the problematic code was:

let item : MyProtocol = Container.instance.resolve()!

When the class loaded up in interface builder, resolve() would return nil, which would end up crashing the rendering agent due to the forced unwrapping.

The code was modified to:

let item : MyProtocol? = Container.instance.resolve()

Everything worked as expected - would get a nil when loaded in interface builder, but would get an actual instance at runtime. However ... shouldn't the type of T be Optional? I've verified at runtime that it's resolving to MyProtocol as I'd really want though, so it looks like the type inference engine is seeing that the return type of the resolve function is an optional of T, so is unwrapping that to determine the type of T is really MyProtocol. The concern is that this behavior isn't by design, so may end up changing in a future version, which would cause all kinds of problems for us. I dug around for quite a bit to see if I could find any documentation related to this, but haven't found anything.

I'm not absolutely sure what you mean, but the type inference seems correct. Its goal is infer a type that satisfies this constraint:

MyProtocol? == T?

and the only solution to that is T = MyProtocol.

It sounds like you're asking if inference takes the variable's full type (MyProtocol?) and "feeds that into" the resolve expression's generic parameter to see what happens. If it did that, the potential T would be a kind of Optional, yes. But inference can't work like that, because it has no particular reason to grab MyProtocol? as a relevant type at all, or to try it out as the value of T, except as part of working backwards to solve the above constraint.

1 Like

It’s my understanding that it is working exactly as designed. Your resolve() function signature states that resolve() returns an instance of an Optional, for which the T? is a shortcut notation. So, when you ask resolve() for an “Optional”, as in the second line, “T” is mapped to “MyProtocol”. In line 1, resolve() is returning an Optional, where “T” still maps to “MyProtocol”, which is forced unwrapped to a “MyProtocol.” As you’ve stated, it crashes when resolve() returns a nil, but will work if resolve() returns a non-nil. From what I understand, that is what forced unwrapping to supposed to do, if it operates on a nil. You’ve got to guarantee that resolve() will not return a nil value at the call site if you want to force unwrap the Optional value.

This is all consistent with the Swift Language Reference. At least you found out the problem during development. It would seem this code could end up being one of those really “head-scratching” moments if you got a crash during runtime because circumstances caused you to return a nil Optional.

It seems pretty straightforward to me. Maybe I’m missing some nuance, which would be helpful to understand.

Jonathan

GeekOnIce
Darrell Bennington

    February 17

I recently did some minor refactoring in a code base involving a initialization of a property on a custom view that was causing some issues when it was loading up in interface builder. The change I did works, but it’s a bit confusing about why it does, and if it’s expected behavior or not. Note that for the below I’m using Xcode 9.2/Swift 4.0.

There’s a dependency injection resolver defined as:

>   func resolve<T>() -> T? {
> //...
> }

Thanks! This makes perfect sense now. What wasn't quite clicking was that I was thinking of this in the way that you explain in the last paragraph where the type of T seemed like it would be ambiguous, because it would "feed into" the generic parameter. Once it's broken down a bit more, then it's obvious that T has to be MyProtocol. Still a lot to learn here!