Should access control on extensions be banned in Swift 6?

As a developer with 7+ years writing Swift professionally, I still find this area of Swift confusing and unintuitive. Here are a few weird scenarios to show what I mean:

Nesting

public extension Foo {
    struct Bar {
        enum Baz {}
    }
}

Quiz: which of these is public?

  • A) Bar
  • B) Bar and Baz

If you guessed A), you're right! However, if you had to read it again to be able to answer, imagine doing this hundreds of times during a regular working day.

In my experience, this causes developer reading fatigue to build up, especially considering more tricky scenarios like:

public extension Foo {
    struct Bar {
        public enum Baz {}
        
        var foobar: String
    }
}

public extension Foo.Bar {
    var foobaz: Int
}

At a glance, you'd guess both foobar and foobaz are internal properties, but in fact only foobaz is! Notice also the incosistency between not needing public for Bar but requiring it for Baz even if indented within it. When iterating on this code, one must always check whether the surrounding context of their declaration is a scoped extension, and adjust their declaration accordingly trying to remember the correct behavior every time. Not great DX.

Protocol conformance

public extension Foo: BarProtocol { // error: 'public' modifier cannot be used with extensions that declare protocol conformances
    func baz() {}
}

So this directly contradicts the behavior exposed above, and is yet another thing to keep in mind when writing extensions. Again, not great DX.

Private extensions

public struct Foo {}

extension Foo {
	private func bar() {}
}

private extension Foo {
	func baz() {}
}

func main() {
	Foo().bar() // error: 'bar' is inaccessible due to 'private' protection level
	Foo().baz() // ✅ — what?!
}

I'm sure there are some other confusing scenarios that come to mind, but here are some that just came to me in a span of a few seconds, which IMO is troublesome and should at least be discussed.

I'd like to know the community's thoughts. Have you ever struggled with this? Does it grant removing scoped extensions in a future major language release?

15 Likes

I pitched this and a variation thereof in 2016 and the core team deemed support for it to be insufficient to proceed:

4 Likes

Thanks for bringing it up, I had no idea.

So given the massive amount of time since, and the fact that the community has grown and the old mailing list format has flourished into a big forum with more diverse and well-formed opinions based on experience, wouldn't it be worth revisiting it?

I'd be all for it, but I personally don't have any strong examples why it should be banned. It's a convenience only feature that I personally don't use and only get annoyed by when reading other code and not expecting it, because of my personal habits.

3 Likes

One breaking change that we could possibly make room for, could be the ability to limit the protocol conformance access.

public protocol P {
  func p()
}

public struct S1 {}
public struct S2 {}

internal extension S1: P {
^~~~~~~~ // explicit access modifier on extensions means
         // the protocol conformance access

  func p() {} // OKAY: not forced to be `public`
}

// this extension limits the conformance to `P` to the `package` level
package extension S2: P {
  ...
}

// error: banned behavior
private extension S2 {}

Not sure if something like this would be very reasonable though.

1 Like

There’s (currently) no such thing as non-public conformance, because as? exists. If you “internally” conform a type to e.g. CustomDebugStringConvertible, a client can still successfully cast an instance of your type to that protocol.

Scoped conformances have been pitched before but they are a very involved feature.

1 Like

Could you clarify what you mean by "involved?"

I mean that it’s not as simple as merely not producing an error, and it’s not even sufficient to teach lookup resolution about access limitations. In the thread I linked to, you and others went into detail about the runtime questions that need to be answered in order to meaningfully define a non-public conformance.

Yes indeed, there are some design decisions to be made, and then things to be implemented in the compiler!

And someone needs to estimate how long that decision making process, implementation, and qualification will take. Then the Core Team needs to agree it’s important to spend that amount of Swift 6’s development time on this feature, and then someone needs to commit to actually spending that time.

My point in linking to that thread was to show that this “breaking change” probably needs more “room” in the schedule than it appears at first glance.

All very true!

As an engineer spending most of my time in code where the difference between internal, @usableFromInline internal and public are critically important, I cannot agree more.

Whether or not a member declaration is public needs to be immediately obvious by looking at the declaration itself. Figuring it out should not require readers to solve a clever little puzzle.

Extension members implicitly inheriting their access level from the extension context has been a constant source of annoyance for me -- it saves the original author from typing a few characters, in exchange for years of unnecessary pain for everyone who needs to understand or modify the code in the future.

The stdlib code base has explicitly refused to adopt the "access modifiers on extensions" misfeature, and I can't help but to dread working on projects that embraced it. I would very much welcome removing it from the language altogether.

Programming languages need to optimize for reading, not writing.

19 Likes

Oh and don't even get me started on this mess:

internal struct Foo {
  public var wat: Int { 42 }
}
3 Likes

This is essentially my reasoning for swift-format banning it and rewriting the extensions to move the modifiers to the individual decls.

The other reason is that once the private/fileprivate distinction was introduced, <modifier> extension no longer means "just pretend all the decls have <modifier> access". Now you have to think about "effective" visibility vs. "written" visibility because private at file scope is the same as fileprivate and that's what the extension's decls end up getting IIRC.

So I wouldn't be sad to see this feature dropped, but after considering all the access modifier changes that happened in early Swift, the fact that people do use this feature makes it a bit of a hard sell to introduce that churn on top of what's already been done.

8 Likes

Not that I think things are perfect, but that’s true even without extension:

struct Outer {
  private struct Inner {
    init() {} // not private, not fileprivate, not internal
  }
  func test() {
    _ = Inner()
  }
}
3 Likes

Yeah I don’t actually mind the “members are as visible as the containing declaration by default (up to internal)” rule, the mistake IMO was dropping the “(up to internal)” part for public extension. Of course, with that caveat added back public extension would become wholly useless, but I don’t see that as a huge loss. Users should be very aware of what public commitments they are making about their API!

2 Likes

The reasoning, as I understand it, was that extensions do not “exist” as an entity and thereby cannot “contain” their members. So instead, the notation {{access-modifier}} extension is a one-of-a-kind “shorthand” totally distinct from the visually similar {{access-modifier}} {{type-introducer}}.

Community members who argued against my pitch to remove this feature stated that, indeed, the requirement to write public in front of every public API is exactly what they want to use this feature to avoid. This is irreconcilable with the countervailing view that all public APIs should be annotated on the declaration itself, but the core team was unwilling to consider weighing in on this absent a consensus being reached.

5 Likes

It seems like the question of enforcing the deliberate annotation of public on each declaration is somewhat separate from the ability to batch-mark a handful of declarations as private or fileprívate by annotating the enclosing extension (a feature which I currently use ubiquitously). Am I understanding correctly that this usage would be removed as well?

I was suggesting that this behavior could be maintained while also upper-bounding everything to internal (i.e., restricting the behavior for public), though I share @allevato's concerns that yet another large amount of churn on access control changes might be difficult to justify.

Right, this is verbatim the narrowly scoped change I pitched instead of eliminating the shorthand feature entirely, which the core team nonetheless declined to allow for review.

1 Like