Introduce type-private access level

In my experience, it has often been the case that one would like to access a private member of a class or structure from within an extension to that type declared in a separate file. The only way to achieve this would be to weaken the member's access level to at least internal, thereby, however, also exposing it to code outside that class or structure.

For example, let's say I have some kind of structure looking like this:

// DataProvider.swift
struct DataProvider {
    func data() throws -> Data {
        return try Data(contentsOf: self.fileURL)
    }

    private var fileURL: URL {
        return URL(fileURLWithPath: "/some/directory/\(self.dataID)"
    }

    let dataID: Int
}

Now, let's say, in another file, I want to extend this structure:

// DataProvider+modificationDate.swift
extension DataProvider {
    func modificationDate() throws -> Date? {
        let attributes = try FileManager.default.attributesOfItem(atPath: self.fileURL.path)
        return attributes[FileAttributeKey.modificationDate] as? Date
    }
}

Of course, without moving the extension to the same source file, I cannot access the fileURL property. So, the only way to access it, would be to make it internally accessible:

struct DataProvider {
    var fileURL: URL { … }
    …
}

This, however, would also expose it to code outside the structure, which is what I may be trying to avoid.

The solution would be to, in addition to fileprivate, introduce a new access level, say typeprivate, that restricts access to the declaring type and any extensions to that type:

struct DataProvider {
    typeprivate fileURL: URL { … }
    …
}

Of course, one might argue that this mechanism could be abused to expose "type-private" members:

extension DataProvider {
    public var publicFileURL: URL {
        return self.fileURL
    }
}

But this would have just as well been possible if the member had been declared internal.

What do you think?

20 Likes

The motivation for this seems to be the want to access private members of a type from a different file than the one where the type was defined (but still within the same module), which to me doesn't see strong enough: why not just add the extension in the same file? If you're worried about the fact that the file could grow, in Xcode we have some tools to organize the components of a file, like the MARK comment, and we can quickly inspect the whole file with the minimap; also, Xcode 14 is introducing pinned scope headers to help with navigation.

1 Like

This would work perfectly for the small little example given. But let's say that extension would require a couple hundred lines of code. And the declared structure would be rather simple.

Also, the extension file may be located at a completed different spot in my project (because it makes sense for it to be there).

I don't think we need to go over, again, why you would want to split your code across multiple files.

4 Likes

I have wanted this feature for many many years.

So +1000 from me.

Having to use a single file is just… wrong. I like creating a separate file per (non trivial) protocol conformance, like:

Person+Encodable.swift
Person+Decodable.swift
etc

And due to lack to private we are forced to make otherwise (type!!)private members internal.

8 Likes

I think this points toward the need for true submodules, wherein multiple files can be grouped together as a logical entity.

Personally, I’d lean toward saying that when we get submodules, the access level fileprivate should be expanded to mean submoduleprivate. That way the number of access levels remains the same and we don’t need to make any substantial changes to them.

10 Likes

This is something I had wanted a while back for a subclass to access internal state of a superclass without that state being made public to all, but would also make sense for an extension, in the same module at least. This would be similar to the protected access level present in some languages.

The suggestion I had been given at some point was to give those members the internal access level, but prefix the names with an underscore _fileURL to indicate they were meant for use by subclasses or extensions only.

I realize that is only a naming convention and not enforced by the compiler. Ideally code outside of the type or subtype would be prevented by the compiler from accessing the member.

One benefit of this approach though, if you are using Xcode, is that it will not suggest symbols that begin with an underscore with autocomplete, so accidental use of the symbols by client code, as opposed to your own extensions or subclasses is less likely.

I bring up that workaround because it has served me pretty well in the absence of some sort of more formal protected or typeprivate access level and you may find it useful.

5 Likes

Why should files matter at all for a modern computer language? Seems quite antiquated to me, like relying on line numbers.

There are of course plenty of reasons you might want to put an extension in a different file. The most obvious one is to keep the file size small.

2 Likes

Wouldn't internal inside a submodule naturally mean the same thing as submoduleprivate ?

2 Likes

There's a good reason why this feature does not exist in Swift. Swift's access levels are strictly lexical and never semantical. If you're trying to access something from another type, then you are either the author of that type and you can use fileprivate/internal access level, or you're not the author of the type and you're trying to access something that the original author didn't intend to be accessed. If you want a method to be overridable, but not callable, you're much better off abstracting it away into another type and then injecting it as a dependency which is stored as a private property, which is better than having a typeprivate access level, since it guarantees that the overriding type can't sneakily make the typeprivate method callable again by expanding its access or making a wrapper method with wider access.

In your case, the url is an integral part of the api, so it has no business being private. If it isn't an integral part of the api and needs to be private, but you still need access to it, then you're establishing a dependency on an implementation detail, which is a violation of the dependency inversion principle, meaning that you're doing something wrong.

The existence of a typeprivate/protected access level is a bad idea on a conceptual level, it shouldn't exist at all. My guess is that it was a very early attempt at facilitating the open/closed principle without violating the single responsibility principle, that didn't go as well as initially envisioned due to the fact that programming languages were in their early stages of evolution and simply didn't know any better.

8 Likes

I think rust's approach to access control is very elegant and would fit well with Swift's lexical access philosophy. It would be nice to specify how high up the module hierarchy is the access spreading to.

5 Likes

Can you explain this a little more? This point may be correct, but I’m struggling to relate this to the existence of fileprivate.

To me, typeprivate seems like a far more logical access control modifier than fileprivate.

I think typeprivate perhaps doesn’t exist because it’s not enforceable as a ~security measure when anyone can write an extension to your type, and Swift doesn’t want to give an impression otherwise? Meanwhile, fileprivate does exist because it doesn’t have that flaw and it’s a reasonable pragmatic feature to offer. But that’s definitely not a lexical access control, as I’d understand the term. (Because the boundaries of the file are not defined in the Swift code.)

3 Likes

Trying to keep things calm here, because another user made a comment just like yours on a thread I was active on. That user subsequently deleted their comment (I got an email), and I respect their right to be forgotten, because I have done much worse. Xcode has been getting some flack lately, and for good reasons. Several months ago, I filed a Radar about how Swift syntax coloring broke in Xcode 12 (or 13), and how I had to use Xcode 11 (or 12) for weeks/months just to be able to work on “groundbreaking” AR research. Furthermore, the Xcode-packaged toolchain is pretty restrictive, so much that I have spent a guesstimated 100 hours preparing workarounds that enable AutoDiff in Swift 5.7. AutoDiff is currently only ever seen in open-source/development toolchains. S4TF is going to change significantly just to adapt for its most common use case, (Xcode) Swift release toolchains.

But as a general Swift development tool, I have a counter argument. I’ve recently been creating Swift bindings for OpenCL - a framework that runs on every platform except iOS. It even runs on Android! This is so not Apple-centric, maybe even undermining their efforts to make Metal the exclusive GPU API. I’m doing this even though I have >1000 hours of experience with Metal and know all of its benefits. And all of this work - from using C Clang modules to writing shaders in OpenCL C - is done in Xcode. That’s the editor I have known for the vast majority of my career as a computer scientist. I saw the awesomeness of VSCode integrated into GitHub’s web IDE, and it makes working on Swift-Colab so much more productive. But I still prefer to use Xcode, and most people contributing to this language do as well (correct me if I’m mistaken).

I think the moderators would agree we need to send a message of collaboration and constructive discussion. My reply here is almost certainly not relevant to this thread, but it makes a point that (maybe) the larger community is interested in discussing elsewhere. Most of my current projects are contributing to changing the reality we have, where Apple iOS app development dominates Swift use cases. I don’t have the time, but maybe someone else can start a thread to discuss areas of improvement with Xcode, start a working group for cross-platform-centricity, guides on using other IDEs with Swift, etc.. I would benefit from that, because in a few months I have to use an Nvidia GPU from Swift on a Windows PC.

Please, someone, open a new thread to discuss these things.

3 Likes

The Swift code is defined by the boundaries of files. fileprivate is definitely lexical. However, fileprivate only exists because a well meaning vocal minority insisted we change the meaning of private to something that ultimately turned out to be worse than the original definition which was synonymous with fileprivate.

Adding the ability to access private members from extensions in the same file papered over most of the differences, but the language would have been better if we'd never added type based private to begin with.

9 Likes

I treat this “encapsulation into extensions” as an anti-pattern.
The purpose of extension is to create convenience methods for various situations, maybe also implement protocols also for convenience. That’s also the reason they don’t have state.
Extensions don’t have an abilities to encapsulate anything and are not designed to do so. Even if the proposed feature would be added, extensions are not restricting access to any aspect of the type and can only expand its interface.
If your type is large that’s a problem with a type, not the problem with the number of lines in the file of the type. Splitting the large type into large extensions will only make it harder to read )in most cases’.

3 Likes

How would you store blocks of related data intended to be used by many programs?

1 Like

I'm not sure I understand the question, but the answer is maybe Swift packages?

The point I was making was that file boundaries shouldn't carry any logical/semantic meaning in a modern programming language. Just like we don't use goto 10 anymore, we shouldn't care what file a specific method or variable is in. The same struct or class should be able to be defined over several files if that's what you want.

3 Likes

Surely this should be up to the implementer to decide?

1 Like

There’re a lot of features that could be easily implemented, but aren’t because they contradict the whole idea of the language. Multiple inheritance, declaring variables in extensions, implicit typecasts or goto.
I think this feature is one of them.

The "strictly lexical" part means that swift doesn't need to perform semantical analysis to be able to resolve access scope. Swift does have a notion of a file (even though it might not be a 1:1 mapping with a file system file (like it's the case for the REPL).

I fully agree with your argument. In fact, I think well-structured Swift code doesn't need to use private access level, opting into fileprivate instead, which is far more intuitive and predictable.