Type declarations in protocols / protocols as namespaces?

I've skimmed a couple of related threads but didn't find the answer to this question, apologies if this has been answered before.

Say I have some protocol Proto and some conforming types, and I want to group these types, and the name Proto happens to be the best name for both the protocol and the namespace.

I might try the following, which will not compile:

protocol Proto {
  // ... requirements ...
}

extension Proto {
  struct Foo: Proto { // ERROR: Types can't be nested in protocols
    // ...
  }
  struct Bar: Proto { // ERROR: Types can't be nested in protocols
    // ...
  }
}

But this workaround compiles:

protocol Proto {}

extension Proto {
  typealias Foo = _Foo
  typealias Bar = _Bar
}
struct _Foo: Proto {}
struct _Bar: Proto {}

and seems to work "as expected" AFAICT, eg I can use generic types and add conformances via extensions etc:

protocol Proto {
  associatedtype A
  associatedtype B
  func req(_ a: A) -> B
}

struct _Foo {}
struct _Bar<V: LosslessStringConvertible> {}

extension Proto {
  typealias Foo = _Foo
  typealias Bar = _Bar
}

extension Proto.Foo: Proto {
  func req(_ a: UInt8) -> String { String(repeating: "*", count: Int(a)) }
}
extension Proto.Bar: Proto {
  func req(_ a: String) -> V? { V(a) }
}

let a = Proto.Bar<Float>()
print(a.req("42!") ?? a.req("123.4") ?? 111) // 123.4

So why can't types be nested in protocols (without jumping through the typealias-hoops)?

2 Likes

I can't say that I know the answer to what your are actually asking, but, I want to be sure your example is understood by me as it is intended by you:

  • struct _Foo exists outside the scope of protocol Proto, right?
  • the compiler interprets extension Proto.Foo: Proto to mean extension _Foo: Proto, right?

So, the structs aren't actually nested inside the protocol. The "typealias-hoops" merely give the visual impression that the structs are nested, right?

But that doesn't answer the question you're asking, which is why can't we actually and truly nest a type within a protocol, right?

1 Like

IIRC @Slava_Pestov wrote an excellent post about it somewhere, but I‘m unable to find it. :/

1 Like

Perhaps this is the one you're looking for: [Pitch] Nested types in protocols (and nesting protocols in types).

1 Like

Right, the typealias-workaround just makes it look like its nested, and after applying the workaround I can write the rest of the code like I wanted (in the non-compiling first example), at least superficially / AFAICS.

Exactly. I'd like to know if it would be reasonable for Swift to support declaring types (structs, classes and enums) within protocols.


NOTE: I'm not asking about
nesting protocols in structs, classes, enums or protocols,
I'm only asking about
nesting structs, classes and enums in protocols.

I think that separates this question from some of the previous discussions I've looked at.

Actually no. The only memory I had was that it was a reply to @anandabits and that @suyashsrijan participated in the thread. This actually helped me digging up the post I meant:

I think to make the nesting work we have to provide some kind of ability to disable capturing outer generic parameters, which is why the type alias workaround already works today.

I don’t think disabling capturing is the right way to go. I’m not familiar with this topic so please correct if I’m wrong, but to access an outside parameter or associated type the outer protocol would need to be a PAT(Protocol with Associated Requirements):

protocol OuterPAT {
    associatedtype Foo
}

extension OuterPAT {
    protocol Inner {
        var a: A { get}
    }

    protocol InnerPAT: Inner {
        associatedtype Value

        var value: Value { get}
    }

    struct Foo: Inner {
        var a: A 
    }
}

So after we’re done with all this, let’s conform a type to OuterPAT:

struct Bar: OuterPAT {
    typealias A = Int
}

Bar.Foo(a: 5) // ✅

OuterPAT.Foo // ❌

If we had some way of specializing PATs:

OuterPAT<String>.Foo(a: “Baz”)

Just an attempt to see if it can be clarified what this means. I think I'll need someone to spell it out for me in a terser way :blush: .

The thread that the above quote is from is originally about nesting protocols inside other types:

I'm asking about the opposite:


However, @Slava_Pestov's post that you quoted above, does seem to answer my question as it addresses the case of nesting struct/enum/class inside protocols, but I'm not sure I understand what the conclusion is :blush::

Does this mean that it won't be a big step to allow struct/enum/class nested inside protocols?

(That is, type declarations in protocols / protocols as namespaces?)

1 Like

@DevAndArtist remember there is also a relevant sub thread in the Improving the UI of Generics thread:

You may find other relevant threads in the cross references the forum automatically adds when a post is linked.

1 Like

Right, I complete forgot that discussion.

My guess: It's not that hard to add those features — but there is no obvious resolution for some cases.
Without generics or associated types, it's all quite straightforward, but what should this do:

protocol Proto {
  associatedtype AType

  class AClass {
    var foo: AType?
  }
}

I'd say treat AType as a generic parameter for AClass — but I wouldn't claim that's intuitive…

On the other hand, if you disallow such "complex" nesting, that's just another limitation which will sooner or later confuse users.

My hope is that we'll see some fundamental changes that bring generics and associated types closer together, but without some explanations from the core team, this is all just speculation.

1 Like

Even if a protocol does not define any associated types, the implicit generic parameter Self is available inside its body. While you cannot refer to Self inside a nested type (it would be interpreted as a shorthand for the nested type itself), you can define typealiases involving Self. eg,

protocol P {
  typealias T = (Self) -> ()

  struct Nested {
    func foo() -> T { ... }
  }
}

I think allowing protocols to be nested inside non-generic classes, structs and enums would be a straightforward extension. Types nested inside protocols on the other hand introduces some difficulties because you'd need to bind the protocol's Self when referring to the nested type from outside of the protocol's body.

1 Like

One solution to this is discussed in the conversation I linked to above.

Oh, right! I completely forgot about the implicit Self type, so I experimented to see how the compiler currently handles similar cases:

// Let's set up our normal - non PAT - protocol Foo

protocol Foo {
  typealias S = Self

  typealias Nested = Bar<S>
}

// Now let's try to access Nested

let nested: Foo.Nested = ...
// ❌ Type alias 'Nested' can only be used with 
// a concrete type or generic parameter base
let fooSelf: Foo.S = ...
// ❌ (The same error...)

Having all this in mind I think that what we could is, allow straight up access to types nested in protocols:

protocol Foo {
    struct Bar { 
        var baz: Int
    }
}

let bar: Foo.Bar = .init(baz: 5) // ✅

But when a protocol captures the protocol's Self:

protocol Foo {
    typealias S = Self

    struct Bar { 
         let capturedFooSelf: S // Captures Foo's Self
    }
}

let bar: Foo.Bar = ... // ❌ Type alias 'Nested' can only be used with 
// a concrete type or generic parameter base

The outer typealias might also be captured inside a method body of Foo.Bar, or even in a method defined inside a extension of Foo.Bar. Since you can't generally "see" function bodies in other source files or modules (both for compile-time performance and resilience reasons), this approach won't scale.

I think I lost you here, could you give an example?

Sure. Imagine you have this,

protocol P {
  typealias T = Self
  struct S {}
}

// in another file or another module...
extension P.S {
  func f() {
    print(T.self)
  }
}

Based on what you wrote above, you would want to prohibit using S as a member type of the protocol itself (ie, P.S) and only allow it to be used as a member type of a type that conforms to P. However, you might not have enough information to make this decision, because you can't "see" the reference to P.T that appears inside the extension method.

Then - at least as I see it - there are three directions we could take:

  1. Treat every type nested type inside a protocol as a non-concrete type - since it may access the implicit Self type. By doing that, nested types inside protocols will be similar to types nested in generic types:

    struct GenericFoo<Bar> {
        enum SimpleBaz {
            case a, b // No generic types captured.
        }
    }
    
    typealias A = GenericFoo.SimpleBaz // ❌ (existing behaviour)
    

    With the proposed behaviour:

    protocol NonPatFoo {
        typealias S = Self
    }
    
    extension NonPatFoo {
        enum SimpleBaz { ... }
    }
    
    typealias A = NonPatFoo.SimpleBaz // ❌ Type alias 'Nested' 
    // can only be used with  a concrete type or generic
    // parameter base
    
  2. Prohibit any access to any types or type-aliases capturing the protocol's Self or a non-PAT protocols:

    protocol NonPatFoo {
        typealias S = Self
    }
    
    extension NonPatFoo {
        enum CapturingBaz {  
            typealias A = S 
            //   ❌   ^~~~~ Cannot access `Self` of enclosing
            // protocol `NonPatFoo` with no associated requirements.
    
            func capturingMethod() {
                print(S.self)
                // ❌ ^~~~~~ Cannot access `Self` of enclosing
                // protocol `NonPatFoo` with no associated requirements.
            }
        }
    }
    
  3. Automatically detect which protocols capture their enclosing non-PAT protocol's Self and mark them as "non-concrete" types, which will require a concrete type to be accessed:

    protocol NonPatFoo {
        typealias S = Self
    }
    
    extension NonPatFoo {
        enum CapturingBaz {  // This type is now marked non-concrete 
            typealias A = S 
            //        ^~~~~ Access to `Self` of enclosing
            // protocol `NonPatFoo` with no associated requirements.
        }
    
        enum SimpleBaz { // This type _is_ concrete
            case a, b 
        }
    }
    
    typealias A = NonPatFoo.CapturingBaz // ❌ Type alias 'Nested' 
    // can only be used with  a concrete type or generic
    // parameter base
    typealias B = NonPatFoo. SimpleBaz // ✅
    

From the three I think the third one is the least plausible. I'm not familiar with compiler architecture and how the third model could be implemented, but I imagine it'd be difficult. As for the second model, I think it could be done but it would be incredibly limiting. Especially when considering that in many cases access to the protocol's Self will be useful, I think it's not the best option. That leaves us with the first model. Now, I understand that many uses will benefit from a protocol being used for namespacing. Still though, with such a model this limitation would not be unique to protocol-nested types. Because as shown in the first example this problem is also present in generic types - the example with GenericFoo and SimpleBaz. As I see it, the problem of using non-capturing nested types without specifying a concrete type is a different problem altogether. I think that a keyword such as concrete - or something to that effect:

struct GenericFoo<Bar> {
    concrete enum SimpleBaz {
        case a, b // No generic types captured.
    }
}

typealias A = GenericFoo.SimpleBaz // ✅ Now valid

So in protocols that could be used as such:

protocol AnyProtocol {
    associatedtype Bar 
    ...
}

extension AnyProtocol {
    concrete enum SimpleBaz {
        case a, b // No generic types captured.
    }
}

typealias A = AnyProtocol.SimpleBaz // ✅ Now valid
1 Like