I'm not sure why that's so bad. Personally I think it's useful to know and to be able to declare the notional public interface of a type, even if that type is not currently being exposed publicly. To me, explicitly-internal
API means something distinct, even if the type is internal and should be used sparingly, to mark API that's not part of a type's primary abstraction. That said, I think the noise of differing opinions here is a demonstration that the design of Swift access control doesn't quite match up with what people want to express with it.
Would not mind if it was removed at all! I've found the access control on the extension keyword very confusing. I guess it saves a few keywords but it's really hard to spot sometimes. Furthermore when you get back to an extension like that and you don't double check you might be writing public code that shouldn't be public etc.
I would be in favour of this change.
My previous question on this subject and several of the responses are a good example of how unclear the tooling and documentation make this area.
What I said in my last comment there still applies:
I always force myself not to use such syntax, and AFAIK thatâs not very welcomed during code reviews because of unclear behavior.
Would like to see it going away in Swift. +1
I use it to distinguish bits of the interface which should really be usable by the rest of the module (the "notional public interface", as you say) from bits which are internal
only because @inlinable
requires them to be
As for access control on extensions, I don't use it, but I also don't particularly mind it. You do need to remember that nested declarations are scoped to the visibility of their parents in order to understand it, but as has been shown, that's not unique to extensions.
Note: Iâm mostly venting in this thread. Even if the language came to its senses today, I know full well Iâd be stuck reading code that fully embraced the current sad state of affairs, forever more. Iâm resigned to deal with it, but I do not like it at all, and I canât help but voice my dissent with passion â especially when past efforts for changing this evidently died due to âinsufficientâ support.
I'm not sure why that's so bad.
I think the public extension
misfeature is bad because it made it difficult to tell the access level of a declaration that doesnât have an explicit setting.
âŚ
// Is this visible from outside? I canât tell. ÂŻ\_(ă)_/ÂŻ
func foo() { ⌠}
âŚ
This âpublic member of an internal structâ misfeature goes the opposite way â it capriciously changes the meaning of an explicit modifier of a declaration. public
sometimes means internal
. (Can it sometimes mean fileprivate
or private
? Frankly, Iâm too disgusted to even try it.)
âŚ
// Is this visible from outside? I canât tell. ÂŻ\_(ă)_/ÂŻ
public func foo() { ⌠}
âŚ
As a Swift engineer, I need to be aware if a declaration is accessible/visible from outside a module. This ornate mess is not helping.
Personally I think it's useful to know and to be able to declare the notional public interface of a type, even if that type is not currently being exposed publicly.
I fully agree with this desire, of course, but I donât think hijacking the meaning of public
was the right way of doing this. Maybe it would have been if types logically defined their own little modules â so type members declared internal
would be inaccessible outside the type â but thatâs not the route Swift decided to take.
Additionally, the type declaration in Swift tends to be very far from individual member declarations: itâs often in a different source file, even. So having to find it to figure out the actual effect of public
feels counterproductive. Itâs even worse than the public extension
misfeature â at least there I donât need to look outside the file.
We do have plenty of other ways to designate an intra-module interface for an internal type. We even have a couple of additional access modifiers for restricting access within a module.
Yes, it can!
And actually, that's the much more pedestrian reason why this had to be permitted: with the new private
, there is no other way to spell a level of accessibility for a member that is equal to that of a containing private
type, so it must be permitted to allow one to use an access modifier that is "more" visible and have that "bounded" by the containing type's visibility. For example:
struct A {
private struct B {
/* ??? */ var v: Int
}
}
To indicate that B.v
should be visible everywhere that B
is, we must write either fileprivate
(which is already more visible than the visibility of B
, and also an access modifier that the core team wanted to see fade away), internal
, or public
. There is no way of actually uttering the real effective visibility of B
once inside B
itself.
Right, and the irony is that it's essentially because we have private
that allows us to designate any arbitrary level in a nested number of scopes as a visibility boundary within a module (such that private
doesn't mean the same thing everywhere inside its own scope) that it became necessary for public
not to mean the same thing everywhere...
While I am not mostly in the position of maintaining widely, publicly used libraries and so perhaps do not share the same visceral passion as Karoy, I'm in wholehearted agreement about the problems with the status quo, and find myself deeply regretting not having participated more (at all, based on my cursory searches?) in the discussions @xwu raised around these problems 6+ years ago.
In particular this shape of complaint:
is critically important and sums up significant chunk of my difficulties working with any programming language. The ability to reason locally about the code you're reading and editing without needing to jump ~randomly throughout and across files is so so nice, and speed bumps in local reasoning ability can feel incredibly disruptive.
In this vein, I'll also add that aside from the ability to determine whether a given function is publicly exposed, IMO the worst problem with public extension
is that it also makes it incredibly easy to inadvertently expose new public
entry points. Rather than being secure in a rule like "if I don't mark it public
, it won't be visible from outside," anyone working in a codebase that permits public extension
must remember "I have to check the containing declaration before adding a helper method here to make sure it's not a public extension
," or else commit to marking all member declarations explicitly, private
, internal
, or otherwise.
In my mind, omitting the access modifier is the specific mechanism for uttering "as visible as the containing declaration" with the caveat that public
must always be explicit. I don't find the lack of an explicit modifier for "as visible as the parent" a particularly compelling argument for allowing the explicit modifiers we have to 'lie' about what effective visibility they actually confer on the declarations to which they are attached.
That said, I find this aspect of the status quo notably less problematic than the problem posed by public extension
since the 'dishonest' modifiers will, at worst, cause library authors to be more careful than they strictly need to, rather than less.
Just out of curiosity (but also maybe could bring some valuable stuff to the discussion) - if you were to design the perfect access control system for a brand new language, taking into account all of the learnings from Swift, could it be described concisely and does anyone have such a description in mind?
There is no fundamental agreement on this: the learnings from Swift are that folks have entirely irreconcilable preferences. For myself, I have yet to see a design that is demonstrably superior to the one we had before the Swift Evolution process.
For a brand new language, you lay out all the use cases, like so:
- "all access private by default", weight: 0.5
- "minimize access control explicit specifiers", weight: 0.6
- "no need to look in other files to know the thing visibility", weight: 0.9
- "parent visibility overrides child visibility", weight: 0.8
... hundreds of these ...
Obviously everyone will have their own idea on importance of a given feature, so it greatly helps at that stage to not have too many different opinions (extreme case - one person choosing the weights based on own preferences).
Once you settle on the table - you consider several designs and pass them through the table, minimising the difference of what you have and what you want, and thus you choose the "best" design.
public
, private
, and internal
are inadequate solutions anyway. You need to be able to target different interfaces to different audiences.
My perfect access control system design would come from Common Lisp or Dylan ancestry rather than from C++. Youâd export a number of namespaces, and your internal declaration names could be assigned to one or more of these namespaces as-is or renamed on export. Clients would import the namespaces of interest, subject to visibility limitations imposed by the tooling system, and could both re-export and rename the imports en masse or individually.
Hereâs an example from Dylan.
Let's maybe keep this discussion focused on the realm of things that could impact the original discussion.
That's too complicated a mental model to work with. This thread is enlightening to me on how badly we've all been educated on how to think simply:
- Every declaration is
internal
until you mark it otherwise. - An access control modifier before
extension
is just a shorthand to avoid writing the same keyword, for every declaration, at the top scope of that extension, and no deeper.
I'm ambivalent about what's being proposed, but public extension
wouldn't be a problem if developers could be helped to understand the rules, which are not documented.
private extension
, though, is broken (and by being so confusing, makes public extension
harder to reason about, by association). To illustrate what others have brought up, above:
private extension Foo {
func baz() {}
}
does not mean
extension Foo {
private func baz() {}
}
Instead, it means
extension Foo {
fileprivate func baz() {}
}
âBanning access control on extensions" would introduce as many fileprivate
s into our code as we used to have declarations in private extensions
.
What should be done, instead, is to change the meaning of private extension
to apply private
instead of fileprivate
. Then, when necessary, we raise access level to fileprivate extension
. That's a lot less new fileprivate
than what's being proposed.
It isn't broken; it illustrates that your second rule isn't correct. The core team decided that the shorthand in front of extension
should reflect the effective visibility of the members inside it, and private
at the top level of a file is equivalent to fileprivate
.
Moreover, this decision was made before extensions were deemed to "extend" same-file private
scopes, and so giving private extension
the meaning that you suggest would have resulted in everything inside being automatically dead code, equivalent to #if false
--which is not a useful feature.
All of this boils down, again, to contending with complications arising because private
at one nesting level means something different than private
at another nesting level, resulting in the existence of an unlimited number of intermediate unutterable "effective visibility" scopes.
I suppose the thrust of what I (along with others in this thread) have been saying is thatâquite apart from the rules being simple or confusingâthere are notable difficulties with the status quo which persist even in the face of perfect understanding. If you are concerned about not exposing a new member publicly then no amount of mastery of Swiftâs access control regime will save you from having to jump to the containing declaration to verify that it isnât a public extension
, and you must remember to do that every time or else you risk making an inadvertent public commitment.
(Similarly, no amount of mastery of the access control rules will let you be certain that an arbitrary public
-marked declaration is actually exposed publicly without tracking down the original type declaration and determining the effective visibility.)
That is my second rule in action. fileprivate
has utility for stored properties, so fileprivate
can't be eliminated. Following that, private
should have no meaning at file scope, aside from private extension
.
But it does. The redundant meaning is applied, at the top scope of that extension, and no deeper, forcing the private
keyword inside both private extension
s and fileprivate extension
s for private
declarations.
"Would have" isn't worth thinking about anymore; private
wasn't useful enough before. Now things work well, except for the inability to cascade private
down into a scope, the way you can with fileprivate
, and the continued conflation of the two.
While I don't agree with the premise of this thread, the following snippet does represent broken nonsense. I'll be happy as long as private extension
is fixed one way or the other. I'll just be happier if it's still usable, rather than being removed.
This is how I do things currently:
// MARK: - fileprivate
fileprivate extension Foo {
func baz() {}
}
// MARK: - private
private extension Foo {
private func bar() {}
}
You don't need to jump or fold/unfold if the IDE tells you what scope you're in.
That would be a useful IDE feature too! But it's not specific to extensions.
While this topic is about access modifiers on extensions, I think there could be more keywords in similar positions that might be re-evaluated. I really don't remember the pitch motivation for some feature idea I had in the past, but I remember that access modifier on extensions and indirect
keyword on the type declaration basically made it impossible. I dropped pitching it and forgot it eventually. indirect
on a type is very convenient, but is it really needed there? It is a potential design space blocker, but again, I don't remember the motivation example anymore.
Thanks for writing this pitch. Access control on extension is confusing.
One more problem is that extensions can be quite long. Developer can accidentally somewhere in the middle add a method that is not assumed to be used as public API. But as extension is public, this method is also public. So explicit access modifier is needed for this method.
We have internal project agreement that ACL is not used with extensions at all. So we explicitly mark everything that is need to be public, and absence of modifier is well known to be internal as default.
So yes, such problems can appear, but ACL rules are flexible and can be set within a team.
Nevertheless I also think this should be revisited in Swift 6 based on the experience of past years.
Swift intentionally does not rely solely on features available via IDE's as it deemed that Swift sources should be inspectable / reviewable in various environments including plain text editors, terminals or web diffing tools. As a striking example of following this guiding principle, Swift uses explicit labeled function arguments (cp with Kotlin that only shows argument labels when viewing in the IDE).