Unexpected property mapping

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.

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

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 :man_shrugging:

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

@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)

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)

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 🛑

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.

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.

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

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?)

It cannot be removed from Swift, but it is not recommended.

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).