Pitch: Allow Protocols to be Nested in Non-Generic Contexts

Full Proposal

Allow Protocols to be Nested in Non-Generic Contexts

Introduction

Allows protocols to be nested in non-generic struct/class/enum/actors, and functions.

Motivation

Nesting nominal types inside other nominal types allows developers to express a natural scope for the inner type -- for example, String.UTF8View is struct UTF8View nested within struct String, and its name clearly communicates its purpose as an interface to the UTF-8 code-units of a String value.

However, nesting is currently restricted to struct/class/enum/actors within other struct/class/enum/actors; protocols cannot be nested at all, and so must always be top-level types within a module. This is unfortunate, and we should relax this restriction so that developers can express protocols which are naturally scoped to some outer type.

Proposed solution

We would allow nesting protocols within non-generic struct/class/enum/actors, and also within functions that do not belong to a generic context.

For example, TableView.Delegate is naturally a delegate protocol pertaining to table-views. Developers should be declare it as such - nested within their TableView class:

class TableView {
  protocol Delegate: AnyObject {
    func tableView(_: TableView, didSelectRowAtIndex: Int)
  }
}

class DelegateConformer: TableView.Delegate {
  func tableView(_: TableView, didSelectRowAtIndex: Int) {
    // ...
  }
}

Currently, developers resort to giving things compound names, such as TableViewDelegate, to express the same natural scoping that could otherwise be expressed via nesting.

As an additional benefit, within the context of TableView, the nested protocol Delegate can be referred to by a shorter name (as is the case with all other nested types):

class TableView {
  weak var delegate: Delegate?
  
  protocol Delegate { /* ... */ }
}

Protocols can also be nested within non-generic functions and closures. Admittedly, this is of somewhat limited utility, as all conformances to such protocols must also be within the same function. However, there is also no reason to artificially limit the complexity of the models which developers create within a function. Some codebases (of note, the Swift compiler itself) make use of large closures with nested types, and they beneift from abstractions using protocols.

func doSomething() {

   protocol Abstraction {
     associatedtype ResultType
     func requirement() -> ResultType
   }
   struct SomeConformance: Abstraction {
     func requirement() -> Int { ... }
   }
   struct AnotherConformance: Abstraction {
     func requirement() -> String { ... }
   }
   
   func impl<T: Abstraction>(_ input: T) -> T.ResultType {
     // ...
   }
   
   let _: Int = impl(SomeConformance())
   let _: String = impl(AnotherConformance())
}

Detailed design

Protocols may be nested anywhere that a struct/class/enum/actor may be nested, with the exception of generic contexts. For example, the following remains forbidden:

class TableView<Element> {

  protocol Delegate {  // Error: protocol 'Delegate' cannot be nested within a generic context.
    func didSelect(_: Element)
  }
}

The same applies to generic functions:

func genericFunc<T>(_: T) {
  protocol Abstraction {  // Error: protocol 'Abstraction' cannot be nested within a generic context.
  }
}

And to other functions within generic contexts:

class TableView<Element> {
  func doSomething() {
    protocol MyProtocol {  // Error: protocol 'Abstraction' cannot be nested within a generic context.
    }
  }
}

Supporting this would require either:

  • Introducing generic protocols, or
  • Mapping generic parameters to associated types.

Neither is in in-scope for this proposal, but this author feels there is enough benefit here even without supporting generic contexts. Either of these would certainly make for interesting future directions.

58 Likes

Can a protocol be nested in an extension? What if it's an extension of another protocol?

Yes, it can be nested in an extension. No, it cannot be nested in an extension of a protocol (no types can be nested in extensions of protocols).

2 Likes

The number of times that "nested protocols" has been brought up and then derailed by the thorny question of "what do we do about generic contexts?" has me convinced that subsetting out this question in favor of making a small but meaningful step in the right direction is the proper way to go. AFAIK the only issue that has ever been raised with nested protocols is the interaction with generics, so I'm excited to see this brought back up and I hope that we can keep the momentum this time!

31 Likes

This looks good to me. While I'd still prefer some form of C++ style namespacing, nothing about this seems to preclude adding that later.

2 Likes

+1000 from me. this is sorely needed, and easily the best news i’ve received today so far :slight_smile:

11 Likes

I'm committed to get this over the line :slight_smile: A couple of things are different this time:

  1. The proposal doesn't include nesting types within protocols.

    Protocols are used so broadly that, personally, I'm a little bit uneasy with it. Consider:

    extension Collection {
      struct String { ... }
    }
    

    With that, you've just redefined what "String" means for every Collection compiled as source with this extension in scope. I definitely would like to add the ability to nest types in protocols in the future (maybe you would have to qualify the type as Collection.String), but it raises enough questions that I think it can be tackled separately.

    The proposal is reduced to the extent that it should be non-controversial, and easy to review, while still containing enough to be worthwhile.

  2. The contributor experience is much improved.

    So, I had a PR for this back in October 2020. Back then, the "getting started" guide wasn't as clear about how to get an interactive debugging environment set up. Here's how it looked backed then. It advised you to build with Xcode for a reasonable debugging experience (which took aaaaaages), and even then, it was really not easy to get an interactive debugging workflow set up.

    Now the docs recommend setting up the compiler within Xcode as an external build system, and provided you just do what it says, you can get a very productive workflow fairly painlessly. If you're not hugely familiar with the compiler's codebase, or LLVM, being able to interactively debug things is seriously a game-changer.

    Thanks a lot to everybody who has worked to improve the contributor experience, and if you've been scared away from the compiler before, I'd recommend giving it another look. It may not be as scary or as painful as you remember.

  3. Compile times are down.

    The new Apple Silicon processors crunch through this code (not an ad, I promise). Again, if you need to feel your way around a little bit, it makes a huge difference.

Personally, I feel the compiler is a lot more approachable than it has ever been. There are some other things I have my eye on after this, as well :wink:

17 Likes

this is really encouraging to me, they are a few SymbolGraphGen bugs i’ve been stewing about for a year or so now, i might actually try fixing them soon.

4 Likes

Types nested inside protocols is tricky. See the discussion on page 147 of https://download.swift.org/docs/assets/generics.pdf.

4 Likes

Is satisfying associatedtype requirement with nested protocol allowed?
In cases like this, before Swift 6, associatedtype B may be satisfied by A.B (== any A.B), although I believe it should not.

protocol A {
    associatedtype B
}

struct AImpl: A {
    protocol B {}
}
1 Like

It seems like there are a lot of circumstances where API designers would like to use a generic type or protocol as a namespace, for declarations such as types, static methods or static properties that are effectively global, and don't depend on the generic context or dynamic class type of the enclosing type. We could have a different flavor of "extension" that just uses the namespace of the type, without really semantically nesting the declarations under the type:

protocol P {}

namespace extension P {
  struct Foo {}
}

let x: P.Foo // This is fine, Foo is just namespaced in P

struct Bar: P {}

let y: Bar.Foo // This is an error, Foo is in the P namespace, not a member of the protocol
11 Likes

I would hate to reserve the keyword 'namespace' for a special kind of extension without also introducing proper namespacing. I feel like every Swift project I work on has at least one empty enum full of static helper functions.

If namespace extension Foo {} extends an existing namespace, that doesn't necessarily rule out having namespace Bar {} declare a new namespace with no type or protocol at all attached.

10 Likes

Agreeeeeeeeeee

let us be true swifties and spell it:

@namespace extension P
{
}
5 Likes

Perhaps; but I think it'd be a separable proposal from this one, and it'd be nice to get some signal as to where that crops up versus scenarios where the generic context of the enclosing type is depended upon.

Which is to say, I guess, that I think @Karl's pitch as-is is appropriately scoped and a reasonable 'resting place.'

12 Likes

Excuse me for suddenly interrupting the conversation.
The main subject is that protocols should be defined inside some data type, right?
I think so too.
Furthermore, I think data types and protocols should be defined inside protocols.
Why is this currently not possible?
My guess is that protocols are treated quite differently than data types inside the compiler.
This is really unsubstantiated speculation as I don't know what's going on inside the compiler.
If protocols were treated as a kind of data type, like abstract classes in C++, this theme should be achieved from the beginning.
For all of you, I may have just said something that was obvious to you.

1 Like

Hi @YoshihiroUnno, please see @Slava_Pestov’s reply above for an explanation to your question.

This is an interesting point. In fact you can already write this today with a typealias:

protocol P {
  associatedtype A
}

struct S: P {
  typealias A = P
}

Notice the absence of any...

@hborla Should we disallow "constraint typealiases" from serving as type witnesses?

EDIT: Holly pointed out that this is actually in the proposal: " Once the any spelling is required under the Swift 6 language mode, a type alias to a plain protocol name is not a valid type witness for an associated type requirement; existential type witnesses must be explicit in the typealias with any :" So the nested protocol case should also be disallowed then.

13 Likes

thank you.
I caused you extra trouble by speaking without fully understanding the flow of the discussion.
I will carefully read the relevant part of the literature that you have taught me, and once I have gathered my opinion, I will make a statement.

1 Like