`override` for protocol conformances?

I just got bitten by a missing ?. Turns out a non-optional version of a computed property is not considered an override of an optional version. e.g.:

protocol P {
    var foo: String? { get }

    func printFoo()
}

extension P {
    var foo: String? { "Default" }

    func printFoo() {
        print(self.foo)
    }
}

struct A: P {
    var foo: String { "A" }
}

struct B: P {
    var foo: String? { "B" }
}

A().printFoo() // Prints "Default"
B().printFoo() // Prints "B"

For inheritance via classes you can use the override keyword to explicitly tell the compiler your intention, and it'll helpfully tell you if you're not actually overriding anything. But that keyword isn't allowed outside of classes. Is there some equivalent that works more broadly?


FWIW, the real-world case where I hit this was conforming to LocalizedError, which has default implementations of all its members, the most important of which is errorDescription which is typed to return an optional String, which is a subtle distinction from typical description properties - like description and debugDescription - that return non-optional Strings. So you can perhaps see how it's a very easy error to make, and you don't find out until runtime and only when you finally hit a relevant error case and only then discover that your error logging is useless because it's the generic "The operation could not be completed" message. :disappointed:

8 Likes

I wrote a quite detailed pitch for this feature back in 2022: pitch, and pitch thread.

It was popular, and amended according to requests from the LSG, but there were no reply after that.

7 Likes

I’ve been bitten by this too (in exactly the same way, with a requirement from LocalizedError). There was a mini-manifesto a few years ago on protocol witness matching, with this case mentioned in the discussion:

2 Likes

I am a big supporter of making protocol conformances explicit. Beyond the mentioned optional v non-optional differences there could be typos or adding new methods to the protocol which would collide with existing methods in the type. If there are some technical difficulties making the absences of the discussed keyword (like "override" or "conformance", etc) an error or 100% reliable, at least it could be a warning and working in most cases – that would still help a lot.

1 Like

Sorry I never responded to your ping @gwendal.roue! FWIW I do still think this is a problem and would love to see the situation improved or solved somehow. I haven’t looked back through the whole pitch or thread but procedurally, the step after pitching a feature and responding to feedback would be to cook up a prototype implementation and open a PR against the evolution repository to signal review-readiness on your end.

2 Likes

Should this be allowed as a match (and go without a warning)?

protocol P {
    func foo(a: Int)
    func bar() -> Int?
}
struct S: P {
    func foo(a: Int?) {}    // should be fine
    func bar() -> Int { 0 } // should be fine
}

At the first glance this look ok and would be similar to the rules we use for subclassing:

class P {
    func foo(a: Int) {}
    func bar() -> Int? { nil }
}
class S: P {
    override func foo(a: Int?) {}    // ✅
    override func bar() -> Int { 0 } // ✅
}

Similarly, shouldn't this be considered as a valid match:

class B {}
class D: B {}

protocol P {
    func foo(a: D) {}
    func bar() -> B
}
struct S: P {
    func foo(a: B) {}       // should be fine
    func bar() -> D { D() } // should be fine
}
following the behaviour of subclasses
class B {}
class D: B {}

class P {
    func foo(a: D) {}
    func bar() -> B { B() }
}
class S: P {
    override func foo(a: B) {}       // ✅
    override func bar() -> D { D() } // ✅
}

Thanks for acknowledging the issue :-)

I think that the compiler can not reliably detect witness near-misses, i.e. perform educated guesses of the intent of the developer. I support this claim with several examples, and scenarios, in the Motivation section of the pitch. This is why I think that only an explicit keyword (override, conformance, @witness, it does not matter) can address this with maximum coverage.

I haven’t looked back through the whole pitch or thread but procedurally, the step after pitching a feature and responding to feedback would be to cook up a prototype implementation and open a PR against the evolution repository to signal review-readiness on your end.

I'd love to help an interested compiler developer if I can :-)

I'm concerned, though, that the pitch did not get enough support from the Swift team. In particular, I remember having to argue a lot with LSG members who insisted that only witnesses wrapped in extension T: P { /* witnesses */ } should benefit from the new feature.

This request, as is, seriously limits the effectiveness of the feature. There are many protocol conformances and witnesses that can not be declared with an extension (such as stored properties, and several other cases listed in the pitch).

The request was heard, though, and the pitch was amended to take profit from those extensions (see the " Precision of Fulfillment Intents" section).

That's where the pitch got stuck.

3 Likes