Introduce type-private access level

This isn’t necessarily just for large types, it can help for smaller types too. This was actually a fairly easy pattern to achieve in Objective-C as you would just create a separate “Foo-Private.h” header file that anything that needed “type private” access could import. This would re-introduce that same flexibility in API design

I have also wanted typeprivate many many many times. There are lots of situations around it, but they all center around balancing the tension between "this thing has private state that should only be accessible by the thing itself" and "this primary implementation of this thing is growing very large and I want to split it into multiple extensions and/or multiple files". But doing that means I can't mark the property as private anymore, because then it's not visible to extensions. And fileprivate isn't that much better, because it still precludes me from organizing extensions into different files.

This is a bit beyond the scope of the specific request here, but I still believe that the holy grail of access level proposals is this one from @Erica_Sadun. It provides the flexibility to define typeprivate and forSubtypesOnly and "friend" types and a whole host of other combinations.

6 Likes

There is a use case for this when implementing advanced custom UI components with AppKit/UIKit, where a custom NSView subclass might want to handle a ton of NSResponder methods. The class can end up getting very heavy, so I split the files into CustomView.swift, CustomView+NSResponder.swift using extensions. I sometimes may want to exclude one of these chunks when targeting another platform (e.g. iOS).

Here’s an Obj-C example from WebKit that attempts to fulfil all it’s view/responder obligations in one 4000 line file: WebKit/WKWebView.mm at main · WebKit/WebKit · GitHub

What I would like is either something similar to typeprivate, to share member access privately across extensions, or some way of indicating to the compiler that a file has been split into multiple pieces, and can be reassembled into one contiguous ’file’ at build time.

I know there are strong ideological opinions on this, an argument being that people will supposedly be encouraged to write worse code… but people are routinely writing even worse code at the moment to work around this.

I've only really run into this issue in Cocoa-land, with classes specifically. Maybe there's an argument to be made to constrain this feature to classes?

1 Like

Also, it's always seemed a bit bizarre for me how access control revolves around files in Swift, although I'm sure there are well thought out reasons for it. If we're insisting on managing access through the filesystem, then why not a folderprivate access level as a means of enabling lightweight submodules?

4 Likes

folderprivate seems actually an interesting idea to me

4 Likes

:wind_face::dash::fire::wood:

submodules that only allow explicit public with implicit internal to submodule

I like @beccadax's take on this in a previous thread on "typeprivate":

So it’s precisely the fact that files are an arbitrary boundary that makes them good for access control. An arbitrary boundary is one you can draw around whatever you want—and that’s perfect for access control.

1 Like

Folderprivate or groupprivate could be interesting, but is it possible to support “groups without a folder” in XCode also?

Using file system folders as a visibility grouping mechanism feels like something you would need to go "all or nothing" on to not confuse users; for example, how Java package names must match the folder structure of the sources being compiled. That's not the case for modules in Swift today, so it would introduce some artificial restrictions; for example, in use cases involving generated code, build systems often generate code outside of the source tree, and code that is conceptually in the same module wouldn't be able to use that visibility level. Maybe that specific use case is an acceptable sacrifice, but it shows the sensitivity of introducing these kinds of non-lexical boundaries.

The way around that would be to have compiler arguments that let you define the groups explicitly, so you could say "these source files belong to this group, regardless of their location", but at that point you're essentially defining something that parallels submodules.

My own personal opinion is that the time folks want to spend/are spending auditing and fine-tuning the access control on their own code is far greater than the actual benefit they derive from it.

7 Likes

A simple example of how this would be incredibly useful is this:

A team of engineers work on a single framework. A portion of this framework is complicated logic that, for necessity, is split among many files. You could imagine, for example, that everything that backs UICollectionView would be more than a single file. You could also imagine that there's some state or behavior exposed by these related types that are intrinsic to how the thing operates, but that this behavior should only be used by the type itself and its support files.

Since this type is split across multiple files, private and fileprivate are not options; you must use internal. But using internal is an indication to other engineers on the team that perhaps they can use this logic themselves, without fully understanding the requirements around it.

In Objective-C, we would work around this by convention using headers of NSFrobnicator_ForThisClientOnly.h or @interface NSFrobnicator (ForThisClient) as a way to suggest that certain functionality had limited scope and applicability.

But ... wouldn't it be nice if we had the compiler to help us? Wouldn't it be nice if we didn't have to remember the details of when stuff is OK to use, but instead could just read the code and see "ah this is typeprivate; I shouldn't and can't use it." Wouldn't it be nice if we had a more expressive access control system so that we didn't have to rely exclusively on comments for this sort of information? (Because we all know how diligent we are at commenting everything and keeping comments up-to-date... :wink:)

I really really like using the compiler to help me remember things, because my brain just doesn't have the capacity to remember everything all the time. I'd love to see the language expand to help me better express these kinds of visibility constraints, just like we use (and love) the type system to express allowed value constraints.

13 Likes

To expand on this, I see it as a gap in the language when extensions provide such an amazing flexibility in code composition and organization -- but is limited by other language features such as the access control.

These two orthogonal language features actually have an intrinsic relationship as how you use one by definition defines how you can use the other.

1 Like

To me this feels like a partial description for protected. I would really welcome that as a subtle addition to access control. It is really useful in Kotlin and Java. It's a shame Apple said no.

The same applies for abstract, but that's off-topic.

1 Like

Why do users of languages like C++ that have all of those various means of access control at their disposal: "protected", "friends", "namespaces" use those means instead of just using files boundaries for access control?

Yeah, I miss that too.

Big +1 on revisiting this topic on the way to Swift 6, it's an opportune time!

I think @davedelong 's use case reflects what many go through. I often find myself promoting things from file private to internal because tying access control to files just isn't feasible when a codebase becomes moderately mature.

Fell in love with Erica Sadun's solution. Any chance we can pursue that? Access Control is one of the few (major) features in Swift that solidified in such a way that wasn't ideal. I'd love to see this seriously reconsidered again if possible.
EDIT: ...so much so that I finally went from years lurking to posting :joy:

4 Likes

I would agree with the idea too and wanted it for so song. My usual case is different from the OP's example and may go too further or go off-topic-ish, but let me try:

I put dozens of codes which are specific to the main type in a file (such as a View or a Model) to the end of the file of the type.
Mostly because it's meaningless to expose them in the other area nor I'd like to see them in auto-completion list.

Like this – resulting a long single file:

// CustomView.swift

// This is the main type of this file
final class CustomView: NSView {
    private let subview1: Subview1 = .init()
    private let subview2: Subview2 = .init()
    private let subview3: Subview3 = .init()
    private let subview4: Subview4 = .init()
    ...
}

//
// MARK: - Subviews
//

private final class Subview1: NSView { ... }
private final class Subview2: NSView { ... }
private final class Subview3: NSView { ... }
private final class Subview4: NSView { ... }

... and usually goes beyond Subview4,5,6... + Helpers and such. The same happens when I work with UIKit, SwiftUI or basically everywhere I'd like to categorize a bunch of codes.

Therefore, I'd like private(Type) or @private(Type). With that the above becomes:

// CustomView.swift
// This is the main type of this file
final class CustomView: NSView {
    private let subview1: Subview1 = .init()
    private let subview2: Subview2 = .init()
    private let subview3: Subview3 = .init()
    private let subview4: Subview4 = .init()
    ...
    // OP's example in this style:
    @private(Self) let somethingCommonInThisType: Any
}
// Subview1.swift
@private(CustomView)
final class Subview1: NSView { ... }
// Subview2.swift
@private(CustomView)
final class Subview2: NSView { ... }

... and so on. Good point is that you can split them up and moreover it's clear where each types belong to.

You can achieve similar by nesting them into the main type's extension in separated files though, it's still accessible from everywhere.

The thing I'm not sure about is the actual necessity to split something among many files because it's complicated: the implication would be that file split makes things less complicated, and I don't really see how. But I might be wrong here, or missing enough real-world examples to justify this.

Suppose that I'm wrong, though: wouldn't folderprivate solve this, in an actually even better way? With something like typeprivate, a developer could potentially use private members of a type from anywhere in the module, while folderprivate would be a much stronger deterrent to "shooting one self in the foot", because it would force a developer to put files in the same folder of the file whose private access is needed, which seems to me a much clearer indicator that there might be a more general architectural problem with the project.

2 Likes

Here's something that I saved for myself as a gist long time ago.

Having all these new access modifier keywords adds so much cognitive load. Why can't we simplify things by only having private, internal, public and open access modifiers?

Anything else would be achieved through access modifier limiters in a hypothetical form of a suffix (or a prefix).

Current New
private private(file)
- private
fileprivate internal(file)
internal internal
public public
open open
  1. Current private would be re-branded as private(file) or private with (file) limiter.
  2. The new private will be type-private by default and does not require an explicit type limiter, like current internal and higher access modifiers already do today.
  3. Current fileprivate would be rebranded as internal(file) which clearly describes the visibility of the modifier that it has today.
  4. [Debatable]: Top-level usage of private or a limited private(file) could be banned in favor of internal(file), or at least non limited private has to be banned.
  5. Last but not least, we could also try and see if closed protocols can be patched in shift as well.
    • public protocol would disallow the user of the framework to conform to the protocol, similar to how public class disallows inheritance.
    • open protocol would act like current public protocol and allow conformances again.
      This would allow library vendors to expose some implementation detail protocols to the public without worrying that the library user would conform some types to it, hence these will be closed to the library.

A phased migration can be purely mechanical.

  • Phase 1: Rename private to private(file), fileprivate to internal(file), public protocol to open protocol and disallow pure private access modifier as well as public protocol in that transition (or warn about the upcoming changes)
  • Phase 2: Introduce new private and public protocol.
1 Like

You are correct that "complicated" does not necessarily imply "multiple files", nor does "multiple files" necessarily imply "complicated".

The point here is that the engineers chose to use multiple files because that was what made the most sense to them from an organizational point-of-view. However, that desire for organization is fundamentally incompatible with the current access control specifiers, since it necessarily requires making parts of stuff too visible to the rest of the module.

Another example of the limitation of the current system is that if I need something to be unit-testable, I must declare it to be internal or public, even if it never supposed to be used outside of the file. fileprivate and folderprivate won't help me there either.

folderprivate might solve it, but it's also forcing a particular style of organization, and Swift does not have official style guidelines. It also unnecessarily conflates the idea of code organization with file organization. If I decide to rearrange my files on disk within my Source folder, that could have the unintended consequence of making my code not compile. That feels wrong to me.

To give you a concrete example: I am currently working on some code that's organized like this:

MyFramework
├ Public API
│ ├ PublicType1.swift
│ ├ PublicType2.swift
│ └ PublicType3.swift
└ Internal Behavior
  ├ Behavior1
  │ ├ PublicType1+Behavior1.swift
  │ ├ PublicType2+Behavior1.swift
  │ └ PublicType3+Behavior1.swift
  └ Behavior2
    ├ PublicType1+Behavior2.swift
    ├ PublicType2+Behavior2.swift
    └ PublicType3+Behavior2.swift

folderprivate will not help me here, because anything that's folderprivate on PublicTypeN.swift wouldn't be available to use inside the Internal Behavior folder.

At a broader level, just like our type system enables me to express API constraints through types that I choose to expose and methods that I choose to write, I believe that our access control system should do the same: it should enable me to describe the level of visibility that I choose for types, members, and methods so that it complements the API/SPI/IPI that I have chosen.

My stance is that until Swift has an official style guide that is enforced by the compiler (like go-fmt), I should be able to choose the style of code that I want to write, so that it maximizes my notion of readability. More flexible access control specifiers is an enormous missing part of that story.

2 Likes

I think the issue is two-fold.

  1. Just because we could solve access control issues by moving things around (or loosening access control) doesn't mean we should have to. How we organize files and what we expose are two fundamentally different things, and having access control dictate where a file should be, even if it isn't naturally where it should belong in a project, doesn't seem right.
  2. folderprivate solves some problems, but ultimately, it pushes the problem up one level. Now instead of having to force things into the same file, now I have to force it into a directory.

I'm not advocating for protected as I understand that can easily be circumvented. I would love something like Erica Sadun's solution, since it improves the testability and flexibility of Swift's access control system.

If Sadun's solution is out of the question, then perhaps we have lightweight submodules and a typeprivate modifier that exposes private members within the same type (read: not subclasses). I also like @DevAndArtist idea.

Friendly question: If we can have breaking changes on the way to Swift 6, this seems like the opportune time to reconsider access control. I'm curious if someone were to put forward a proposal and implementation, would the Core Team be open to revisiting access control?

2 Likes

This kind of choice doesn't seem much different from any other choice that any other developer could make about using Swift: providing the option to make a particular choice is not necessarily the best thing for a programming language. In fact, I'd argue that the strength of a language, and by extent of a set of software engineering practices, is putting constraints on the choices that can be made. That's why I think we need compelling real world examples to justify this granularity and semantics in access control. Or, as you mentioned, a good, opinionated style guide.

You can achieve the example you mention

by separating the public interface of PublicType# from its internal one, and then use the latter in the Behavior# files. With something like typeprivate, you could mark as such the things you want to access in the Behavior# files, but then internal wouldn't make sense anymore, and typeprivate would essentially replace it.

I can see that internal would still be useful when decomposing features in different types, instead of in extensions, such that typeprivate would be essentially the internal of extensions, but are you sure it matters that much? With internal we are achieving the same thing, decomposing a feature in multiples bunches of code within a module: the fact that the decomposition is done with extensions or other types doesn't seem so important as to justify working on new, different access levels.

I honestly find much more relevant and useful to be able to limit the visibility of things to actual "physical" boundaries, like files and folders.

I can see that it doesn't seem right. But sometimes the way things seem is caused by the way we're used to think about them. Personally, in the last few years I strongly reevaluated the way I feel about access control: it's something that seems to be at the foundation of software development (at least some of it), but when asked to provide rational reasons to justify some of its quirks, and provide unbiased criticism, I gradually discovered that it doesn't matter that much (apart from the distinction public/non public in a module), and that the file boundary is much more useful that it might seem.

I don't really have a strong opinion on this matter: it's unlikely to change the way I write Swift (apart from some very specific things, like UI elements in a view controller), but I think it's an area where strong, compelling examples are really necessary to justify the work in certain direction.

2 Likes