Malconformance

I'd like to bring the following situation to attention, to give the opportunity for thought on whether or not it is problematic (in my view, it is).

The topic is protocol conformance, and more specifically the fact that it is possible to incorrectly conform to a protocol (using the wrong type on a function) when that protocol has a default implementation.

For a practical example, consider LocalizedError's errorDescription, which requires a type of String?. We could accidentally implement it using String and the compiler will happily let us do it and say nothing:

struct ErrorX: LocalizedError {
    var errorDescription: String?
}

struct ErrorY: LocalizedError {
    var errorDescription: String
}

ErrorX(errorDescription: "foo").errorDescription
(ErrorX(errorDescription: "foo") as LocalizedError).errorDescription
ErrorX(errorDescription: "foo").localizedDescription

ErrorY(errorDescription: "foo").errorDescription
(ErrorY(errorDescription: "foo") as LocalizedError).errorDescription
ErrorY(errorDescription: "foo").localizedDescription

It is only much later, during runtime, assuming you have a good QA team in place, that you might discover that in fact, you are getting quite odd localized errors out of your application. Where you expected to see foo, instead you are seeing "The operation couldn’t be completed. (__lldb_expr_3.ErrorY error 1.)".

After much debugging you finally realize that your mistake was having forgotten a ? on var errorDescription: String.

This seems like a situation that the compiler should be able to help with; and I'm honestly not even sure what it means practically to have overloaded methods of the same name and parameter signature (although with a different return type).

1 Like

Users can also run into this if the protocol requires a particular type and they have their own type of the same name locally. I've seen this happen most often with Result.

However, I'm not sure the compiler can offer a general rule here given that it's value to have such overloads. It almost seems like this should be a linter rule, if the linter is powerful enough to detect such cases.

Although, it may be help the "shadowed type" case if the autocomplete was able to detect the shadowing and complete the fully qualified name to disambiguate. Is that even possible @xedin @ahoppen ? Unfortunately that doesn't fix issues where autocomplete doesn't work or isn't used.

This is kind of shadowing is something which has been our radar for a while now but it requires substantial changes to fix and most likely a proposal to adjust semantics of the language in some corner case situations.

IMHO examples like @lhunath's and its reduced version:

protocol P {
  var test: String? { get }
}

extension P {
  var test: String? { nil }
}

struct X : P {
  var test: String = "hello"
}

_ = X().test

should actually be considered ambiguities without a contextual type because it wouldn't be possible to tell which test got picked in this case without deep understanding of how ranking and other aspects of the solver work and even further - when contextual type is implicit e.g. parameter type or similar we'd emit a warning asking to provide explicit type via as coercion to make sure that users understand what got picked if overload choices differ in optionality only.

Sure. I was thinking more of the autocomplete for the local type shadowing issue.

protocol SomeModule.P {
  var test: Request { get }
}

extension SomeModule.P {
  var test: Request { .someValue }
}

// In my File.swift

import SomeModule

struct Request {}

struct X : P {
  var test: Request = ...
}

In that case X will conform but the apparent conformance won't be used. Now, if the autocomplete could suggest var test: SomeModule.Request to avoid the shadow, I think that would help in many cases. I don't think this case runs afoul of the language issue you mention since it will default to the local type, I just wondered if the compiler could make such a suggestion in the first place.

There is no way to do that right now unfortunately, that why I tried to make a point about non-trivial changes we'd have to do support this. Type-checker simply doesn't know that SomeModule.Result exists in that context because lookup stops after finding first match for Request in its own module.

1 Like

Ah, makes sense, thanks.

Honestly, it doesn't seem inappropriate to me for this to warn based only on the fact that X.test shadows the protocol requirement. The rule would be something like 'you get a warning if you try to declare a protocol conformance on a type/extension where you also shadow one of its requirements.' You could then silence by moving the conformance to its own extension X: P {} to be clear(er) about the fact that you want to use the default implementation.

We could make the warning even more sensitive, and warn whenever a user shadows a protocol requirement, but that would be more difficult to silence—your only option would be to rename the shadowing member.

Near-miss checking already exists for extensions declaring conformances.

There are unique consequences to extending this to types because in the general case (and, indeed, in the example you quote) the shadowing member can be a stored property, which cannot (currently) be moved to an extension. As you point out, you could still silence such a warning by moving the declared conformance itself in a separate extension, but then you would lose this near-miss checking for any other stored properties which are protocol requirements.

I would love to see this revisited should we ever get stored properties in same-file extensions (and I would love to see that pitch revisited in turn, partly because that's the final missing bit that would make fileprivate redundant and therefore possible to retire from the language like the core team once mentioned).

1 Like

Thanks, I thought I remembered that being a thing but couldn't figure out why it wasn't triggering here. :sweat_smile:

This still seems to me like it would be a strict improvement from the status quo—right now users don't get any of that feedback for conformances declared on the type declaration itself.