Calling private methods from extensions in separate files

I see that some developers really like to implement protocol conformances as extensions in separate files, but then Swift prevents access to private members that are declared in other files of the same type. The result is that developers will turn those members from private to internal which makes me quite sad. Is there a good reason that Swift isn't allowing access to private members when the extensions are declared in the same module?

3 Likes

Swift’s access modifiers are deliberately lexical, not type-based. The access modifier that allows use throughout a module is internal, within a file fileprivate, and within a lexical scope private.

Other languages may offer type-based access modifiers, but that is deliberately not the case for Swift.

3 Likes

Well the point of my post is that I am arguing this is bad because real-world codebase ends up using what is clearly the wrong access modifier, as those members become exposed to other types in the module once they become internal. Developers, as human beings, have a liking in tidying up things in their own places, this seems to have been overlooked.

You asked a question about the rationale—I just replied with the answer. There’s been nothing overlooked in this area; you can read up on the extensive prior discussions by using the handy search function.

You have apparently lived with some very different developers than I have. :nauseated_face:

6 Likes

I don't think it is uncommon, for example swiftlint has a rule enabled by default which complains when a file is over 400 lines. Developers end up splitting up files in extensions. Personally I think this is silly, but I can't stop developers from doing that, and I still haven't seen a good reason to prevent private access from extensions within the same module.

I tend to disagree: private is already (partially) type-based access control, so the current system is not as clean as it used to be...

I don't think there should be yet another level, but it might be possible to improve the situation without a major change in the compiler:
Not everything has to be enforced, and changing the behavior of tools with annotations could be enough. If autocompletion hides methods in certain contexts, and tooltips showed some warning, nothing else might be required (and such a system could be used for more situations — like methods which should never be called directly, but just overridden by subclasses...).

It's not clear to me that internal is a worse access modifier. If anything, I'd say that the addition of scoped private was a mistake, and the language was better when private meant what fileprivate means now.

7 Likes

hmm, internal does seem to be worse, members are now exposed to the whole module.

I'm with you on this one, it feels wrong. IMHO type-based access modifiers would have been a better choice for swift. Unfortunately that ship has sailed.

There's a genuine problem with your alternative, though, which is that allowing extensions in other files to access private members is pretty much the same as changing private to internal anyway.

Anyone can simply bypass your privacy restriction by implementing conformances to a made-up protocol, and thereby give effective access to everything that's supposed to be private.

At least one of the purposes of private is for a type implementation (in a single file) to be able to guarantee that there are no invisible dependencies on its private details from other filestypes in the same project. If you (the implementer or the type) are going to trust everyone else (the clients of your type) that way, then your type may as well be internal instead of private.

I agree that there's a difficulty to be solved here. My preference would be for some kind of hierarchical sub-module system. Still, there's been a lot of discussion about solutions over the years, and no real convergence.

8 Likes

I think the alternative would be something like what other languages call protected: visible via limited interactions with the type. This could include subclasses, which is the typical usage, but could also include extensions in other files. So the property wouldn't be visible to consumers of the type, even internally, but would be to any subtype or extension. Personally, I would find this very useful to maximize the cleanliness of projects. Currently I'm forced to either allow things to be inappropriately visible internally or put everything in one file, including rather large subclasses.

4 Likes

For this particular aspect the difference between fileprivate and private could have been used..

1 Like

I understand there are different approaches that different people might want to promote, but the key issue is that the only way for the implementer of a type to keep control of its private details (while spreading those details across multiple implementation files) is some scheme that requires the implementer to grant access to the additional files.

So, protected could be problematic if anyone could subclass the base class in order get access to its private details.

I don't see exactly how fileprivate helps that much. Maybe something like private("additionalFileName.swift") could be a way of granting access.

This is basically a security issue.

Since the only way to access something that is internal is to be compiled into the same module (@testable import notwithstanding, and that requires that the module being imported opt-in to testable imports), why wouldn't you trust those internal clients?

You mention "security issue" below, but if there's a major risk from declarations being too visible within the same module, i.e. if a component can't trust the code given to the same compiler invocation, isn't that illustrative of a much more significant breakdown in the code review or build process? And if it really is that sensitive, then maybe it should be isolated in its own module?

The recurring desire to finely audit access control of the code within one's own module strikes me more as adversarial toward extremely uncommon hypothetical scenarios rather than serving a practical purpose for everyday Swift usage.

7 Likes

It's not really about trust or security in the security sense. It really does happen, though, that other people not familiar with the design of your class might add some kind of accessor to derive something from a private property, for example — and they get it wrong, or introduce a cross-type dependency you don't know about.

My feeling exactly. I'd love to have a lightweight module system, such as hierarchical sub-modules defined within Swift itself, that provide boundaries around which inadvertent misuse of private details can be prevented.

IME, Swift's access control* is such a delight to use that the old type-based system feels ancient no matter what languages I switched to. I can't quite put it into words, but Becca's word strongly resonate with me:

* No, not you private. You're stealing fileprivate's place again... :expressionless:


Digression aside, I'm not sure I ever have a problem with one part of the project accessing other parts because of them being internal. I do get somewhat paranoid in the beginning, but nothing ever comes out of it, even as the projects grow in size.

Though all these talks about security and trust make me think I might be missing out on some fun people are having. I mean, access control is such an easy thing to bypass that they're usually more about convenience than concerns.

7 Likes

In my experience, internal or public are almost always the right access modifiers. Most Swift projects I've seen use these two modifiers extensively. The ability to access one type's properties and methods from within another type has never become an issue for me.

I only use fileprivate or private if there's absolutely no reason to access a property or method outside of the type/file (i.e. @State properties in SwiftUI).

I’ve found that local Swift packages are a good way to organize modules hierarchically.

2 Likes

A different non-file based approach could be a more stricter form of extension that is being granted.

struct Thing {
    vetted extensions Foo, Bar // preliminary new syntax
    private var x = 0
    private func y() {}
}

// same file as Thing
extension Thing { // normal extension
    func yy() {
        x = 1 // Error, x is private
    }
}

// same or different file
vetted extension Thing(Foo) { // preliminary new syntax
    named extensions Baz // preliminary new syntax
    private var w = 1 // variables allowed
    private func z() {
        x = 1 // ok
    }
}

// same or different file
vetted extension Thing(Baz) { // preliminary new syntax
    private func t() {
        x = 1 // ok
        w = 1 // ok
    }
}

// same or different file
vetted extension Thing(AttemtToBreachSecurity) { // Error, not allowed
}

real-life analogy would be: not everyone can extend my house, only those whom I granted that permission. And if the vetted builder wants so they can hire a subcontractor.

The added benefit is an ability to add new variables in these vetted extensions.

The problem with this, and any other attempt to allow scoped multifile access to a member, is that anyone can just add a public version of the member in one of your allowed extensions. It's no more "secure" than the internal version, but half as convenient.

struct Thing {
    vetted extensions Foo, Bar // preliminary new syntax
    private var x = 0
    private func y() {}
}

// same file as Thing
extension Thing { // normal extension
    mutating func yy() {
        x = 1 // Error, x is private
    }
}

// same or different file
vetted extension Thing(Foo) { // preliminary new syntax
    named extensions Baz // preliminary new syntax
    private var w = 1 // variables allowed
    private mutating func z() {
        x = 1 // ok
    }
    // This is going to be legal no matter how you attempt to specify
    // who can do what.
    public var pubX: Int {
        get { x }
        set { x = newValue }
    }
}

Any added safety by using private is completely illusory if you have any method of accessing a private member from another file.

And the only security you get from single file private is that you only have to vet one file to make sure no one is exposing things they shouldn't be, instead of every other file in the project.

4 Likes