SE-0404: Allow Protocols to be Nested in Non-Generic Contexts

+1 I would love to be done with writing typealiases just to achieve similar outcomes.

2 Likes

+1

I’ve missed scoping protocols many, many times. A consequence of not having this is that protocol names are longer than necessary and at the same time less clear. A big gap in Swift is filled by this proposal.

Hopefully the standard library can be revised with this in mind for Swift 6.

A quick reading.

Strong +1.

My only reservation is the non-generic scope limit, but I'm assuming supporting generic scopes would complicate the implementation enough to miss the next Swift train out of the station.

I hope in the future we can revisit generic scope support.

+1. I have long been annoyed that this doesn't work, mainly for namespacing reasons.

3 Likes

+1 on this proposal

I think that generic protocols would be really cool and useful, and if they were added it feels like it could be simple to add support for generic contexts.

i wrote up the following intending for it to be a starting point for discussing a new language feature, but as i was finishing it i realized it was really more of an argument for why this proposal should pass.


in C++, i always spent (wasted?) a lot of time thinking about memory safety.

in swift i find that i spend (waste?) a lot of time thinking about lexical hierarchy.

here’s an example of a hypothetical library product that vends one single overwhelmingly important type, and a supporting protocol to be used with that type:

struct Barbie
{
    enum ID {}
    struct Instant {}
    struct Camera<Film> where Film:BarbieFilm 
    {
        struct Photo {}
    }
    ...
}
protocol BarbieFilm
{
    var id:Barbie.ID { get }
    var timestamp:Barbie.Instant { get }
}

i say product and not module because putting both of these definitions in the same module precludes using Barbie as a module name. and in light of that i think it’s helpful to list what i think are some reasonable goals regarding the usability of a library product:

  1. a module’s name should be a description of what the module contains, and not a description of the fact that it is a module. so we really want to use this library as import Barbie and not import BarbieFramework.

  2. library consumers should standardize on one import spelling. in other words, if there is a module named Barbie, but most of the library’s critical functionality is actually @_exported from an umbrella module like BarbieFramework, and import BarbieFramework is the prescribed way to consume the library, we do not want people to type import Barbie and then wonder why nothing is working.

    this is also a way of restating goal #1, if the “public-facing” umbrella module has the simplest name amongst its constituent targets, no one will accidentally import a component of the library when they mean to import the whole thing. (think NIO vs NIOCore.)

  3. library API should obey namespace shadowing rules. in swift, this means if you have a module named Barbie and it also has a top-level type named Barbie, then every other (public) type vended by that module must be lexically scoped to Barbie. things that cannot be nested (like protocols) need to be @_exported from another module with a different name.

  4. library consumers should never utter an internal module name. this is just another way of saying that library consumers should not have to care about a library’s internal dependency graph, and they should not care what module a type is “really defined in”.

    at the pure source code level, this is an equivalent statement to “your library should have a very low probability of name collisions with other code”, because if you never experience a name collision, then you never have to qualify a reference with a module name in the first place.

    and yet “what module a type is actually defined in” is kind of unescapable, it shows up in URLs, it shows up in symbol links, and people get it wrong all the time. so even if you have the most well-designed lexical structure possible, a naming scheme so holy that it will never collide with anyone else’s names, you still have to care about what your inner modules are named, you can’t just prefix them all with underscores.

anyway, i was originally envisioning some kind of “public-facing module name feature”, but then i realized these “usability principles” are really just an elaborate mitigation strategy for the problem described in goal #3: if it weren’t for the fact that we need to reexport protocols from hidden modules with @_exported because they cannot live in the the user-facing module, then we don’t need to lower the rest of the module’s implementation into a hidden sibling module, in fact we can really dispense with the whole “hidden modules” nonsense entirely.

i for one can’t wait for the day where i can stop obsessing about module naming because i will finally be able to declare a protocol in the same module as the code that uses it.

3 Likes

One of the rare times I chime in on the forums but I want this so badly that I felt the need to say +1 +1 +1 would absolutely love to see this.

3 Likes

+1
Whatever argument there is against this feature is moot because you can already bypass this restriction:

protocol _SomeProtocol {}

enum Namespace {
    typealias SomeProtocol = _SomeProtocol
}

This update just lets us declare Namespace.SomeProtocol directly without going through the hoops

11 Likes

+1

Long have we suffered excess identifier noise

1 Like

Biggest +1 I think I’ve ever given. Holly and Karl may now be my favourite people in the world (especially given the future direction of allowing types to be nested in protocols)!

3 Likes

@hborla Perhaps it would be nice to have a dedicated voting post in proposal reviews, allowing people to unleash their "+1" energy? just a thought

Now to the point of the proposal: one additional future direction could be to introduce into the language a way to define a namespace without defining a new type (not protocol/struct/class/enum/actor).
At its simplest, this could be C++-style namespaces. A more sophisticated option would be a type-like entity whose members are all implicitly static.

protocol MyProtocol {
  static var foo: Int { get }
  static func bar() -> String
}

a_fancy_keyword_like_namespace MyScope: MyProtocol {
  let foo: Int = ...
  func bar() -> String { ... }
}

More or less corresponds to what is now expressed through enums without cases:

enum MyScope: MyProtocol {
  static let foo: Int = ...
  static func bar() -> String { ... }
}

This is not a super-important feature, since it can already be expressed in the language. However, I find it strange to have a pattern that forces you to declare enums without cases.

1 Like

LGTM, this is definitely a worthwhile addition to Swift. It is something I've really missed when coming to Swift all the years ago and still often bump into missing it.

I'd also definitely welcome the future direction to allow Sequence<T>.Iterator where Iterator is the nested protocol inside the Sequence. It's fine to leave this to future proposals -- although I hope they will come sooner than later :slightly_smiling_face:

3 Likes

might be getting a bit ahead of myself, but what are the chances of this making it into the 5.9 release?

As per Ben's post re: the 5.9 release process, only "major showstoppers" are eligible for being picked onto the 5.9 branch at this point.

3 Likes

I think including a poll would reinforce the misconception that Swift evolution reviews are votes. It's also very helpful for reviewers to briefly elaborate why they're giving the +1, even if it's just a sentence, and a poll would encourage people to not leave a review comment with their elaboration. But I don't see this as a problem; "+1" comments are welcome!

15 Likes

Ok, noted. +1 then

Thanks everyone for participating in this review! SE-0404 has been accepted.

Holly Borla
Review Manager

10 Likes

I do like this feature. It’s so frustrating to have to think First Level since every thing can be Inner-ed.

1 Like

I reckon that also holds for point-releases? If so, then too bad that we have to wait until 5.10 to use this wart-removal.

Given the implementation of SE-0404 has not landed on main because it isn't yet complete, it is premature to discuss which release of Swift the feature will land in.