Scoped Conformances

I mis-read the proposal. Post is collapsed for posterity.

+1 to the feature. I have also been wishing for something like this. As an API author, there are times when I don't want to expose aspects of functionality as part of my public API. For example, if I want to expose some sort of opaque token type, I sometimes don't want to also expose that it's Comparable or RawRepresentable or whatever, because that's not how I'm intending clients to use it.

I'm not a fan of this syntax. In an ideal world, I would be in favor of a rule like:

Conformances inherit the visibility of the declaration.

So:

// conformances to Comparable and CSC are public because this is a public declaration
public struct X: Comparable, CustomStringConvertible { ... }

// conformance to Wrapper is internal because this is an internal declaration
internal extension X: Wrapper { ... }

This avoids introducing any new syntax and avoids confusion around doing something like this (which would be syntactically allowed but semantically rejected):

internal struct X: public Comparable { ... }
3 Likes

At the level below the language level, it does not matter that the public conformance uses non-public names in it. Consider the following, which is obviously OK:

internal func compareHelper(lhs: X, rhs: X) -> Bool { ... }

public struct Y: Comparable {
  public static func <(a: Self, b: Self) -> Bool {
    return compareHelper(lhs: a, rhs: b)
  }
}

The conformance of X to Wrapper is internal, but its conformance to Comparable is public, and would be treated everywhere (in the compiler, in the module interface etc.) as such.

Then the conformance of X to Comparable would be conditional with the same conditions.

But if one of those conditions is the Wrapper conformance how do users know when that condition is met and when it is not met?

We could extend the value witness table representation with a list of private conformances. Dynamic casts would first look into the VWT. Existing public VWT symbols would point to a VWT with an empty list of private conformances. When we need a VWT for a given scope where the existential is formed, we can use the default VWT if there are no private conformances. If there are private conformances visible in that scope, we create a new VWT dynamically by copying the default one and attaching the list of visible private conformances.

Sorry, could you provide an example?

Sure! One big reason is that it's consistent with the intended behavior of generic parameters as described by @Douglas_Gregor in this post, which is the only sensible model, AFAICT, for dealing with conflicting conformances in multiple modules. IIUC it is also consistent with implicits in Scala and other precedented language features.

How would you describe a public conformance that relies on non-public names? i.e. in your example Wrapper is internal. How would users of X know when it conforms to Comparable ?

Oh, do you mean, “how would such a conformance be described to users of X's public API?” In the example given, the conformance of X to Comparable is unconditional, so users would see it as a regular conformance to Comparable. If it were a conditional conformance, you still wouldn't be allowed to use private names in the where clause of its conditional conformance, so again, it would be documented in the ordinary way.

Yeah, that's the easy case ;-)

My instinct has been telling me that we could simply take the most-public implied conformance of the ancestor—why force users to disambiguate if we can avoid it?—but I'd like to have a discussion to analyze the implications of a choice like that more carefully. Certainly, forcing explicit disambiguation is a conservative choice; we can always take a less-strict approach later.

1 Like
@davedelong mis-read the proposal. Post is collapsed for posterity.

That is the rule today, and unfortunately it doesn't cover our intended use case. We have a public protocol, and we need our clients to be able to conform their public types to that public protocol without exposing the conformances. Thus the explicit fileprivate qualifier on the conformance.

I mis-read the proposal. Post is collapsed for posterity.

This is exactly the situation I'm talking about, but I'm sure I didn't state the "rule" well enough because I'm not well-versed with compiler terminology.

I'm saying we should make this syntax work:

public protocol Fooable {
    func doFoo()
}

public struct Bar {
    let baz: String
}

internal extension Bar: Fooable {
    func doFoo() { print(baz) }
}

Right now this dies with several errors:

error: method 'doFoo()' must be declared public because it matches a requirement in public protocol 'Fooable'
    func doFoo() {
         ^

note: move the instance method to another extension where it can be declared 'public' to satisfy the requirement
    func doFoo() {
         ^

error: 'internal' modifier cannot be used with extensions that declare protocol conformances
internal extension Bar: Fooable {
^~~~~~~~~

If we change the compiler to allow this, then we don't need to invent new syntax at all.

@davedelong mis-read the proposal. Post is collapsed for posterity.

Thanks for explaining. I'm afraid that for your suggestion to cover our use case, it would require changing the meaning of existing language constructs. Today, the access specifier preceding the extension sets the default access for the definitions within the extension, so what you wrote is equivalent to:

extension Bar: Fooable {
    internal func doFoo() { print(baz) }
}

IIUC you're now saying it should mean something like (in my proposed syntax):

extension Bar: internal Fooable {
    public func doFoo() { print(baz) }
}

But I'm pretty sure that is not a backward-compatible change for existing code.

I mis-read the proposal. Post is collapsed for posterity.

Putting a visibility modifier on an extension is shorthand for putting the same visibility modifier on every otherwise-unspecified thing within the extension.

Thus:

internal extension Bar: Fooable {
  func doFoo() { print(baz) }
}

should be equivalent to this (using your proposed syntax):

extension Bar: internal Foo {
  internal fun doFoo() { print(baz) }
}

So my question is:

Can we not just use the visibility of the extension to also apply to the visibility of the conformance?

It is not a backwards-compatible change. Today, internal in internal extension does not apply to protocol conformances. So implementing your suggestion would change meaning of existing code.

I'm not sure I follow. The syntax I'd like to see isn't allowed by the compiler, which means there is no existing code that has it.

Oh -- yes you're right, it is a special-case error today. I guess we could allow it, I don't see a problem technically, except for not being able to continue teaching internal extension as a syntax sugar -- because there is no syntax that it desugars to.

@davedelong mis-read the proposal. Post is collapsed for posterity.

But that wouldn't be legal in today's language, as you'd be satisfying a conformance to a public protocol (Foo) with an internal definition. That, as I said at the end of the pitch, is a different feature, and it raises some issues that our proposal does not. We're not prepared to try to work out those issues, nor are we sure that if they were worked out, our use case would be covered.

1 Like

Aha! This makes me think I've completely mis-interpreted what the pitch is about then. I'll go re-read and see what I got wrong.

public struct X<T> { ... }
internal extension X: Wrapper where T: Wrapper { ... }
extension X: Comparable, CustomStringConvertible where T: Wrapper { ... }
// or
extension X: Comparable, CustomStringConvertible where Self: Wrapper { ... }

So the above example would not work, correct? I know this is mixing threads a bit, but what are the implications of that for Structural? I was thinking that the compiler would provide a memberwise conformance to Structural which would sometimes have to be conditional on type parameters, but maybe I misunderstood.

That sounds sensible as well. If nobody comes up with any clear downsides maybe it is the best choice.

This conditional extension X: Comparable is only usable in scopes where the T: Wrapper conformance is visible. I'm not sure if that answers your question?

With the pitch as written, that's correct. It's possible the pitch could be extended to allow that, but we wanted to bite off a self-contained and easily-analyzed chunk of the space, and...

I know this is mixing threads a bit, but what are the implications of that for Structural ? I was thinking that the compiler would provide a memberwise conformance to Structural which would sometimes have to be conditional on type parameters, but maybe I misunderstood.

…as far as I know, we don't need this capability. A conformance to Structural merely means that you can destructure the conforming type, not necessarily each of its members.

extension Bar: internal Foo {
    func doFoo() { print(baz) }
}

Can we only allow internal and explicitly not support private or fileprivate scopes? I don’t see a reason why we should go into the business of supporting private and fileprivate. Perhaps I am missing something but I think marking the conformance as internal should be sufficient to address the core of the proposal with out getting into the issues of private vs fileprivate.

1 Like

Would it make it possible to have two variables that hold the same instance, with the same dynamic type, with the same static type, but behaving differently? Very scary.

// a.swift
class Foo { }

// b.swift
extension Foo: fileprivate CustomStringConvertible {
    fileprivate var description: String { "short description" }
    var short: CustomStringConvertible { self }
}

// c.swift
extension Foo: fileprivate CustomStringConvertible {
    fileprivate var description: String { "long description" }
    var long: CustomStringConvertible { self }
}


// main.swift

func staticType<T>(of value: T) -> T.Type { T.self }

let foo = Foo()
let a = foo.short
let b = foo.long

assert((a as? Foo) === (b as? Foo)) // they are the same object
assert(type(of: a) == type(of: b)) // they have the same dynamic type
assert(staticType(of: a) == staticType(of: b)) // they have the same static type
assert(a.description != b.description) // but they behave differently?
3 Likes