Namespacing protocols to other types

I do miss nested interfaces in Java, which is great for scoping delegates. Here's something I'd like to do in Swift:

class FileInformationProvider {
  protocol Delegate: class { }

  weak var delegate: Delegate?
}

class FileManager: FileInformationProvider.Delegate { }

Nesting protocols could really benefit for the same reasons of nesting classes, structs, and enums. Protocols shouldn't pollute the global namespace when only relevant to a certain type like delegates.

13 Likes

I have an implementation that I worked on a while ago that allows you to nest a protocol inside a class, struct or enum (but not a protocol). However, it won't allow you to capture the parent type's generic context. So, it's meant to be used purely for name spacing purposes, which I believe is what 99.9% of the people want. The generic stuff can come later, perhaps when we have generic protocols.

I kept the implementation simple so we can focus on what most people need and not have to worry about other complicated stuff like capturing generic parameters or associated types of the parent type and other issues.

I can write a proposal for it and pitch it.

7 Likes

Please do! :clap: Does it allow nesting other types in protocols (if the protocol does not use Self or associated types)?

No, it only allows you to nest a protocol inside non-generic classes, structs or enums. It doesn't allow you to do the reverse. I don't think it should be allowed personally because everything inside a protocol (except typealiases) are requirements, however we could allow it to be declared inside a protocol extension instead.

protocol A {
  enum B {} // error
}

extension A {
  enum B {} // ok
}
1 Like

It‘s kind of the same thing, I‘m fine with allowing nested types in protocol extensions but the protocol should support these types im protocol requirements as well.

protocol P {
  var b: B { get }
}

extension P {
  struct B {}
}

I don't think this makes sense. Protocol extensions extend all conforming types in addition to the existential. See the recent discussion about nesting and namespacing in the Improving the UI of Generics thread for more of my thoughts on this topic.

1 Like

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: https://github.com/apple/swift/pull/23096

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.