vykut
(Victor Socaciu)
1
Hello,
I recently encountered this unexpected behaviour when working with protocols and optionals.
For some unknown reason to me, the compiler decides to map a property to its default protocol implementation when the object that contains the property is optional
Minimum reproducible example
protocol UIComponent {
var onTap: (() -> Void)? { get }
}
extension UIComponent {
var onTap: (() -> Void)? { nil }
}
struct Component: UIComponent {
let action: () -> Void = { }
}
extension Component {
var onTap: () -> Void { action }
}
let component: Component = .init()
let optionalComponent: Component? = .init()
dump(component.onTap) // - (Function)
dump(optionalComponent?.onTap) // - nil -- uses the default implementation of UIComponent
As you can see from the example, the type of Component.onTap is not an Optional compared to the requirement of the protocol.
If I change the type of Component.onTap to an optional closure, I won't get a nil value anymore.
tera
2
This does look like a bug.
protocol UIComponent {
var onTap: (() -> Void)? { get }
}
extension UIComponent {
var onTap: (() -> Void)? {
print("UIComponent.onTap called")
return nil
}
}
struct Component: UIComponent {
let action: () -> Void = { }
}
extension Component {
var onTap: () -> Void {
print("Component.onTap called")
return action
}
}
let component: Component = .init()
let optionalComponent: Component? = .init()
dump(component.onTap) // Component.onTap called
dump(optionalComponent!.onTap) // Component.onTap called
dump(optionalComponent?.onTap) // UIComponent.onTap called 🤔?!
optionalComponent!.onTap() // Component.onTap is called
optionalComponent?.onTap() // Component.onTap is called
Although I wonder why you didn't make Component's onTap matching the protocol exactly:
var onTap: (() -> Void)?
in which case the bug is not triggered.
1 Like
vykut
(Victor Socaciu)
3
The code I wrote above only serves as a way to reproduce this unexpected behaviour.
The actual scenario is a little bit more complex, and involved not breaking around a hundred unit tests in our massive codebase 
xwu
(Xiaodi Wu)
4
No, it is not an implementation bug. In Swift, implementations of protocol requirements cannot differ in optionality. That is, if the requirement is a property of type T?, then it must be fulfilled by an implementation with a property of type T?, not T:
protocol P {
var v: Int? { get }
}
extension P {
var v: Int? { 42 }
}
struct S: P {
var v: Int { 21 } // this does not fulfill the requirement
}
func f<T: P> (_ t: T) {
print(t.v as Any)
}
f(S()) // prints "Optional(42)"
In far future language versions, we may consider changing this—see the discussion on a draft protocol witness matching mini-manifesto. This would be a source-breaking change, as users rely on the current behavior.
1 Like
vykut
(Victor Socaciu)
5
@xwu it makes sense in your example, and that is what I would expect too.
but consider these 2 lines of code, following your example:
let s = S()
let optionalS = Optional(S())
print(s.v) // 21
print(optionalS?.v as Any) // Optional(42)
To me, this is really unexpected. (and I believe for others too)
xwu
(Xiaodi Wu)
6
It is very difficult to reason about, and that is why it's not recommended to overloaded two properties of the same name but different types. Consider the following—also very confusing!
protocol P {
var v: Int? { get }
}
extension P {
var v: Int? { 42 }
}
protocol Q {
var v: String { get }
}
extension Q {
var v: String { "Hello, world!" }
}
struct S: P, Q { }
let s = S()
let optionalS = Optional(S())
print(s.v as Any)
print(optionalS?.v as Any)
tera
7
Then this is bug:
dump(optionalComponent!.onTap) // Component.onTap called 🛑
dump(optionalComponent?.onTap) // UIComponent.onTap called
optionalComponent!.onTap() // Component.onTap is called 🛑
optionalComponent?.onTap() // Component.onTap is called 🛑
Jumhyn
(Frederick Kellison-Linn)
8
I haven’t looked too closely at this behavior but it doesn’t seem obviously buggy to me. It’s not that surprising that the same name ends up resolving to a different overload when used in a position that expects a name as opposed to a position that doesn’t.
tera
9
Refactored / simplified:
protocol P {
var value: Int? { get }
}
extension P {
var value: Int? { 42 }
}
struct S: P {
var value: Int { 21 }
}
let optionalComponent: S? = .init()
print(optionalComponent!.value) // 21
print(optionalComponent?.value) // Optional(42)
If not a bug then it's very confusing feature.
Jumhyn
(Frederick Kellison-Linn)
10
I suspect what’s happening here is that when you use optional chaining instead of force unwrapping, the result of the overall expression is always going to be optional, and so the overload that produces an optional value is going to be considered a better match than the one that doesn’t.
Perhaps a bit unintuitive but I think it’s ‘correct’ given how overload resolution/ranking works. And as Xiaodi notes, it’s a good reason to not rely on return-type overloading.
2 Likes
tera
11
I see, indeed:
let x: Int = optionalComponent!.value
let y: Int? = optionalComponent?.value
print(x) // 21
print(y) // Optional(42)
This is in part a consequence of overloading by return type (is return type overloading needed in swift or could we remove it at some point?)
xwu
(Xiaodi Wu)
12
It cannot be removed from Swift, but it is not recommended.
tera
13
Indeed we can't remove it. Can we soft deprecate overloading by return type and have a warning when it is used? That would discourage new uses of it (and suggest user refactor the old uses).