Namespacing protocols to other types

What does not make sense to you?

To me it‘s the same as if you would write typealias B = P_B where P_B is a pseudo nested type but declared globally as nesting is currently not allowed. Right now you would only be allowed to do so if the protocol itself does not use Self or associated types. Is there anything I am missing?

struct P_B {}
protocol P {
  // can live inside extension as well
  typealias B = P_B
}

protocol Q {}
extension Q {
  struct A {}
}

struct S: P, Q {}

S.B.self == P.B.self // == P_B.self
S.A.self == Q.A.self

Yes I have a PR for this: Allow protocols to be nested in non-generic contexts by karwa · Pull Request #23096 · apple/swift · GitHub

I got fed up pinging people every week and left it. Personally I think it's obvious enough that it doesn't need to go through swift-evolution.

You can't inject a type declaration into multiple parent declarations. That doesn't make sense. It makes sense for typealias because they are transparent and evaluate to the specified type. If what you suggest were allowed the implementation would need to actually inject a typealias into the conforming types. I would rather not allow that. It would be better if this was done explicitly when desired.

Why would you not want conforming types inherit the nested type? This makes perfect sense to me as it‘s what you want if you already writing type aliases. If the nested type also would capture the associated type (in the future) you‘d want the nested type to be inherited by the conforming type as it will provide a concrete type for the protocols associated type.

This is the natural behavior I would expect from nested protocol types. You can still use them directly if they don‘t capture anything. I don‘t see any disadvantages. Do you have any examples in mind that would be harmful if we did it that way?

We already have this behavior on classes.

class X {
  enum E {}
}
class Y: X {}

X.E.self == Y.E.self

That’s great! No need for me to push my implementation then.

The reason I often want to nest a type inside a protocol is because it is a helper type for the API of the protocol. I do not want these helper types to be added as a nested type of every conforming type. If you want that typealias it’s not a big deal to have to write it manually.

How would you do it? You already can‘t avoid it on classes and a type alias in protocols is basically a nested type. If you had to do it on protocols it would require you to come up with a different type name yet again as typealias B = B would not make sense to the compiler. I think you expect a non natural behavior here. (If I‘m the one who has it wrong, please feel free to correct me.)

A type nested inside a generic type can be "lifted" to a top-level type whose generic arguments and requirements are the concatenation of the outer type and inner type. Eg,

struct Outer<T> where T : Equatable {
  struct Inner<U> where U : Sequence, U.Element == T { ... }
}

is the same as this:

struct OuterInner<T, U> where T : Equatable, U : Sequence, U.Element == T { ... }

Now suppose you have a type like this:

protocol Outer {
  struct Inner<U> where U : Sequence, U.Element == Self { ... }
}

Since a protocol Proto has a single generic parameter Self with a single requrement Self : Proto, this is in fact equivalent to writing:

struct ProtocolInner<Self, U> where Self : Outer, U : Sequence, U.Element == Self { ... }

The only difference is that for some type C conforming to Outer, I write C.Inner<Foo> instead of writing ProtocolInner<C, Foo>.

This also means that referencing the nested type as a member of the protocol itself is not valid! Eg, Outer.Inner<Foo> is not valid because Outer does not conform to itself. Even if the generic requirements of Inner do not reference Self or associated types, the bodies of its methods (or methods defined in extensions) might, so in general we can only allow types nested inside protocols to be seen as members of conforming types, and not of the existential type.

Note that all of the above applies equally well to nested type aliases as well as nested nominal types. We already worked out most of the edge cases for nested type aliases so allowing nominals to be nested inside protocols won't be a big step.

Edit: This is about struct/enum/class declarations nested inside protocols. Protocols nested inside other protocols runs afoul of the "protocol cannot be in generic context" rule, which is more difficult to resolve. Right now we assume a protocol only has a single generic parameter Self, and there are multiple independent ways of generalizing this, all of them rather difficult.

8 Likes

Sorry for not responding to the pings. If you want to take this further, I would suggest adding some SILGen, IRGen, execution tests and metadata demangler to make sure everything works end-to-end.

1 Like

Since @Karl already has a WIP implementation, it would be better for him to take this further. If he's no longer interested, I can put up my PR which has tests as well. IRGen has some issues with emitting metadata for nested types inside protocols extensions, which is something I need to fix.

I can skip that if we don't want to have nested types inside protocol extensions.

Take a look at, eg, IRGenModule::emitClassDecl(), and how it calls emitNestedTypeDecls(D->getMembers()). It might be a matter of just cargo-culting this.

Good luck!

1 Like

I think Karl's PR is about protocols nested inside non-generic types. Types nested inside protocols will require a bit of additional work. Today we assume the parent type of a nominal type is a nominal type derived from its lexical parent, but instead it has to be a conforming type in the case of a type nested in a protocol. Then you have to teach TypeBase::getContextSubstitutions() to use the parent type as a Self substitution in this case.

1 Like

One more thing. From your particular case it seems to that you again want just to disable any capturing behavior just to get the namespacing.

@Slava_Pestov do you think something like this would be worth having in Swift. A very abstract example would be:

extension Equatable {
  @doesNotCaptureAnything // straw-man attribute
  struct NotReally {}
}

// You can then safely use it like this:
let value = Equatable.NotReally()

struct G<T> {
  @doesNotCaptureAnything
  static var value = 42

  @doesNotCaptureAnything
  enum Nested {}
}

G<A>.value = 0
G<B>.value == 0 // true

I can see other use cases to allow nesting types (including protocols) in generic types without capturing any of the outer generic types. This would allow you to have G<A>.Nested.self == G<B>.Nested.self and provide nice namespacings.

In the post I linked upthread I laid out a design that would let you extend any Protocol to extend the existential only. You would declare the type in one of these extensions. This design also introduced extensions to some Protocol to exited confirming types only. You would place the typealias in one of these extensions:

protocol P {}
extension any P {
    struct S {}
}
extension some P {
    typealias S = P. S
}

Of interest, the syntax that post used for nesting protocols and types inside a generic type without capturing the generic context is also to extend any T rather than placing them directly inside the type and waving hands about the generic context.

If T was generic then any T would also include the generic types no? I‘m not sure this avoids capturing these things. (Please correct me if my implication is wrong.)

@Slava_Pestov that's an interesting post, thank you! It explains that it would make sense to declare a type inside the protocol when we want to capture Self as an implicit generic parameter of the type. However, that is often not what we actually want. This means that each conformance binds the generic parameters to a different Self and is therefore a distinct type.

While that semantics makes sense and I guess I wouldn't mind if we support it, I don't think it is actually what people want most of the time. As with nesting inside generic types, often what people want is a type that does not capture the generic context of the protocol and is only namespaced. That is why I like the idea of extending the existential and declaring nested types inside these extensions.

I don't see why we would use an attribute for this. Placing declarations that should not capture the generic context inside an extension of an existential seems like the most natural approach to solving this.

No, the existential any Collection does not have any generic parameters. It is a concrete type that is no more generic than Int.

I don't agree with this, this behavior is exactly what I wanted in most of the cases. I want the nested type in protocol to not capture from the outer protocol (if it does not use Self or contain any associated types this is trivial), and I want the conforming types to inherit these nested types.

If you had this example:

protocol P {
  var b: B { get }
}
extension P {
  typealias B = Int
}

You certainly don't write P.B for the protocol requirements in the conforming type like so:

struct S: P {
  var b: P.B = 42
}

Instead you just want to write var b: B = 42. However you still want to re-use the namespacing for simple type nesting like so:

var test: P.B = 42 // this already works just fine today

To me your solution seems to be limited to protocols only (as this discussion originated from that direction), but it does not solve the same problem in non-protocol generic types. The attribute example I provides is just 'one solution' to the problem, but as far as I'm concerned it would cover both situation (generic types and protocols with Self or associated types).

Here is just one example ripped out of our library where I badly need nesting like mentioned above.

public protocol Peripheral {
  typealias Scan = PeripheralScan
  typealias Firmware = PeripheralFirmware
  typealias Service = PeripheralService
  typealias Characteristic = PeripheralCharacteristic
  typealias CharacteristicEndpoint = PeripheralCharacteristicEndpoint
  typealias ServiceEndpoint = PeripheralServiceEndpoint
  typealias Update = PeripheralUpdate
  ...
}

This is self-contradictory. @Slava_Pestov has explained that the natural semantics of declaring a type inside a protocol is to capture Self (even when Self is not used in the implementation of the type). He explained how this is a natural parallel with how types nested in a generic type capture the surrounding generic context whether they use it or not.

I gave an example that shows how you can declare a type nested inside a protocol _that does not capture Self`` by placing the types inside extension any Protocolinstead ofextension Protocol. If you don't want conformances to prefix the type names with ProtocolI showed how you can add typealiases inextension some Protocol` that make the types directly available.

Why do you say this? My solution is to introduce existentials for generic types as well as protocols. any Array would be an Array with an unknown Element. This allows us to nest declarations inside Array without capturing the generic context.

You want these to be identical for each conformance, right? @Slava_Pestov explained why they would be distinct types for each conformance because they would capture Self if they were type declarations instead of typealiases.

Could a tastefully placed underscore be used to indicate not capturing generic context?