Allowing non-public methods to satisfy public protocol requirements

The way that I would want to write the example in the original post is:

extension ImportantProto where Self: SpecialCaseImplementationDetail {
  public func difficultRequirement() -> String {
    return "\(muchSimplerRequirement())"
  }
}

That way the default implementation is within an extension of the protocol whose requirement it satisfies, and it doesn’t require any sort of relationship between the two protocols.

However this currently raises an error, “Cannot declare a public instance method in an extension with internal requirements”.

3 Likes

I think @Nevin 's form should be allowed as well.

I like Nevin's example too, but I still find that public inconsistent. I can see two different angles to address that:

  • Significant public: Within a constrained extension of a public protocol, a default implementation may be marked as public even if the constraints include types that are not public. This indicates that the member may be used to satisfy requirements; it still cannot be invoked directly from outside the access level it would have had. The same applies to internal protocols.

  • Looser checking: When checking conformance to a public protocol, all default implementations in extensions of that protocol are considered, even if they are not marked public. The same applies to internal protocols.

(with "default implementation" meaning "member of a protocol extension that has the full name as a requirement")

Both rules have the same behavior with regards to static vs. dynamic dispatch, i.e. clients cannot use static dispatch if they would not normally be able to see the implementation.

I like these a little better than both my initial proposal and the alternate form proposed by Itai, limiting the funny behavior to places that obviously look like default implementations. From the earlier responses, it seems like "Significant public" is probably the preferred solution?

I'm having trouble parsing the wording of the rule. Could you walk me through it with some simple code examples, maybe?

Would it potentially make sense for the compiler to synthesize an anonymous public “marker protocol” with no requirements, which SpecialCaseImplementationDetail implicitly refines and to which other conformances and refinements are prohibited?

That way the extension from my previous post could be exported as if it were:

extension ImportantProto where Self: MarkerProtocol {
  public func difficultRequirement() -> String {
    // The compiler knows that every type conforming to MarkerProtocol
    // also conforms to SpecialCaseImplementationDetail, and magically
    // allows the call to muchSimplerRequirement even though it is not
    // actually available on MarkerProtocol
    return "\(muchSimplerRequirement())"
  }
}

This keeps SpecialCaseImplementationDetail encapsulated within its home module, and only the anonymous empty marker protocol is made public.

The programmer, of course, would still just write:

extension ImportantProto where Self: SpecialCaseImplementationDetail { … }

+1 for significant public rule

Sure, here's a pile of examples:

// My original example.
// Not permitted under either new rule (with or without 'public').
internal protocol SpecialCaseImplementationDetail {
  func muchSimplerRequirement() -> Int
}
extension SpecialCaseImplementationDetail {
  public func difficultRequirement() -> String { ... }
}
// Nevin's example.
// Under "Significant 'public'", this is permitted.
// Under "Looser checking", you can't write 'public' here,
// but the implementation would satisfy the requirement anyway.
internal protocol SpecialCaseImplementationDetail {
  func muchSimplerRequirement() -> Int
}
extension ImportantProto where Self: SpecialCaseImplementationDetail {
  public func difficultRequirement() -> String { ... }
}
// Reverse of Nevin's example (notice which protocol is extended).
// Not permitted under either new rule (with or without 'public').
internal protocol SpecialCaseImplementationDetail {
  func muchSimplerRequirement() -> Int
}
extension SpecialCaseImplementationDetail where Self: ImportantProto {
  public func difficultRequirement() -> String { ... }
}
// Inheritance-based - questionable.
// It doesn't really make sense to say
// `extension BaseProto where Self: SubProto`
// and have that be different from
// `extension SubProto`
// so maybe we should allow this too, because of the inheritance.
internal protocol SpecialCaseImplementationDetail: ImportantProto {
  func muchSimplerRequirement() -> Int
}
extension SpecialCaseImplementationDetail {
  public func difficultRequirement() -> String { ... }
}

This would be a reasonable implementation, sure, though I have something a little simpler in mind closer to Slava's existing workaround. It doesn't really affect the static vs. dynamic dispatch bit, though: if the marker protocol is empty, it's not carrying the necessary information to call a method implemented in terms of the actual internal protocol. (And it can't just copy the requirements over because those might reference non-public types.)

Anyway, I think the important part right now is to figure out the language-level semantics. We can worry about the implementation later as long as it's not going to be a surprise / disaster on the performance side.

1 Like

Thanks, I think I understand what you're getting at a little bit, but I'm still a little confused about both proposed solutions here, in terms of what's actually proposed:

Sorry, "not permitted" here means difficultRequirement() does not satisfy a protocol requirement for a distinct, public protocol ImportantProto in the case of a type that is trying to conform to both protocols, yes?

Why would writing public here be prohibited (or anywhere else)? Isn't the idea behind SE-0025 that it's never an error to use a higher access level (except in the case of extensions with their own access level, a discussion for another day)?

Sure, questionable here. If we distinguish between extension PublicProtocol where Self : InternalProtocol and extension InternalProtocol where Self : PublicProtocol, though, then it's fine IMO to distinguish between extension BaseProtocol where Self : RefinedProtocol and extension RefinedProtocol. And if we don't distinguish between the latter, then I don't see how we can justify distinguishing between the former.

Yes, that's correct.

Ah, you're correct according to the formal rules of SE-0025. I just didn't actually implement it at the time. (DaveA reported this a while back as SR-4695, but I haven't seen any other complaints about it.)

This would normally be a bad thing, but in this case it means that no one is doing it by accident. On the other hand, though, it means that it's not compatible with what people are doing in Swift 4 today.

You're right that they're equivalent in terms of what the compiler calls the "requirement signature". However, while the first pair is essentially symmetric, the second pair is not, and I would guess that everybody would be using the shorter syntax here.

This is convincing me that special-casing extensions of the public protocol may not be the best way to go. :-(

Given the weirdness around inheritance, I'm going to go back to the version Itai suggested and tweak it a little:

Within a protocol extension, a member may be marked with a broader level of access control than the protocol being extended if that member matches a requirement with that level of access control.

That means that for the examples above,

  • "My original example" is disallowed.
  • "Nevin's example" is allowed.
  • "Reverse of Nevin's example" is allowed.
  • "Inheritance-based" is allowed.

In all cases, public is required, and in all cases the internal protocol's participation can still block some optimization. What do people think of this?

5 Likes

The solution makes sense to me semantically, and I'm +1 on that latest version. I've got a few implementation questions below, but they're mostly immaterial to the proposal.

Am I correct in thinking that the optimisations blocked (namely that dynamic dispatch is required) is the same as when the implementation for a protocol requirement is on a type rather than an extension – that a virtual lookup is required to figure out what the type's implementation of that protocol method is?

e.g.

public protocol SomeProtocol {
    func requirement()
}

public struct SomeStruct {
    public func requirement() {
         print("Do something")
    }
}

let protocols : [SomeProtocol] = [SomeStruct()]

protocols.forEach { $0.requirement() } // dynamically dispatched

Because if so, that seems completely reasonable. However, it does seem like when the type is known, even from an external module, calling a protocol method on that type (that's fulfilled by an internal extension) should use two static dispatches: one to a public method on the type, and then one forwarded by that public method to the internal method in the protocol extension (as in my example above).

It may be that that's a worse implementation than always doing a dynamic dispatch – I'm more wanting to check that I'm understanding this correctly, and that it doesn't strictly have to be a dynamic dispatch in these scenarios.

That's a possible implementation strategy that would avoid the dynamic dispatch, yeah. I'm not sure if it's a good idea or not, though—it does increase the ABI surface of the library beyond what happens for truly-public default implementations. @Slava_Pestov, what do you think?

This latest definition sounds good to me, and matches what I would expect as a developer given the examples above — as long as the protocols are related in a clear way like this, this seems like a no-brainer.

1 Like