Implement Alvarez & Monteiro's "typeprivate", possibly with one or two extra restrictions

In 2016 there was this great proposal: https://github.com/goncaloalvarez/swift-evolution/blob/master/proposals/NNNN-introduce-typeprivate-access-control-level.md

The idea is to add "typeprivate" so you can put extensions into separate files to improve code readability without having to resort to "internal". (Currently any extensions that access private stored properties must all be in the file that declares the type, and "fileprivate" must be used. However this can lead to bloated code files.)

If the original "typeprivate" is seen as too "loose," here are two extra restrictions that would make "typeprivate" unable to be stealthily abused.

Extra Restriction 1: Typeprivate extends only to Explicit Extensions

To avoid any file in the module being able to get around typeprivate restrictions by simply using an extension, we could add some simple extra restrictions to be enforced by the compiler.

The first extra restriction would be a rule that only allows typeprivate access in files whose name starts with "NameOfParent+".

For example given "Foo.swift" with:

public struct Foo { 
    public typeprivate(set) var bar: Bar? = Bar()
}

Then the only way to have typeprivate access from outside this file would be to have a file like "Foo+Quxable.swift" {

extension Foo: Quxable {
    func clearBar() {
        bar = nil
    }
}

That way, we can separate extensions out into separate files without having to make private things internal. But code files that aren't explicit extensions would not get typeprivate access.

Extra Restriction 2: Explicit Extensions that extend typeprivate may not do anything else

If that's not enough, then we could further restrict the first restriction by specifying the following additional rule.

For any type "Foo" that uses a typeprivate access level, any files "Foo+ExtensionName" may only contain extensions on Foo (but may not contain other top-level declarations).

That way, it would be impossible for another type to bypass the typeprivate level by sneaking code into explicit extension files. (They could only do it by putting an extension in the main file.)

My Recommendation

To me, the second restriction seems a bit much.

But I like the first restriction because it would let you still be able to have a CODEOWNERS file that covers any files with typeprivate access, because git lets you use wildcards :D

I think this would be a nice, additive, quality-of-life improvement that would encourage better code readability and more compact code files, without resorting to declaring things as internal just to do so.

This could be especially beneficial in allowing new capabilities for CoreData types, where you have an extension in a separate file to separate the auto-generated code away from the manual code. In such an arrangement you cannot have private properties accessed in the extension which is very annoying and makes CoreData less safe.

Thoughts?

My Responses to the Likely Counter-Arguments:

To those who might say, "what is wrong with long code files full of extensions," I would say if you are using extensions to keep code-generated files separate from manually written ones, using a single file is bad because the generator will overwrite your manually-written code. As well many people prefer several smaller files to one big one.

To those who might say, "We already have too many access-level specifiers," I would counter that no, we don't. Five is actually not very many of anything. Five is hardly even "several." It's basically just a handful. It's a single-digit quantity. And if six is really too many, then we could deprecate "fileprivate," or add the proposed file-naming rule above to slightly expand "fileprivate" (instead of using it to restrict "typeprivate"), because even that will not break anyone's code (since it's a purely additive change). Either way there would be nothing that could force you to use this new access level. You could simply pretend like it wasn't even there, or tell all your devs to never use it. Or maybe we could even have a compiler flag to disable whatever access levels you don't want to get used in your project.

To those who might say, "If you need to use something like typeprivate it means you are architecting your code poorly." To that I would counter, no, it just means I want to break up a really long code file into multiple files for the sake of readability. I hope you can give a better counter-argument.

That's not to say I think this is a perfect idea—maybe there is some reason why it couldn't work? But I really think (especially with the first extra restriction mentioned above) this would be very nice to have, and it also (to me) feels a lot more sensible than "fileprivate" (which seems to encourage the practice of adding a bunch of stuff to a single file).

6 Likes

I'm in the "changing the meaning of private was a mistake, but we're stuck with it" camp, and feel that if you need access in multiple files then you should leave it internal, so this is a clear -1 from me.

9 Likes

Does Swift use the name of a source file for anything like this today? This seems like it would have far-reaching implications, from compiler compatibility across multiple operating systems and filesystems (:bellhop_bell:) to generated source files that don’t fit with this naming convention.

I think that typeprivate definitely solves a problem I’ve run into, but is there another way to solve this without using the filename?

2 Likes

Why should the programming language care about "files"?

I get that we're stuck with files as a way of separating sections of code, but it feels like a very arbitrary kind of boundary that should not change the meaning of the actual code.

This idea has been brought up enough times, and the reasons why it does not comport with Swift’s design for access modifiers reiterated sufficiently, that I think we should add it to the list of commonly proposed/rejected ideas.

In brief, Swift’s design deliberately opts for lexical scopes, each one a superset of the next. This was a deliberate choice and really not up for modification. I don’t think anything has changed since previous discussions that merits a rehashing of existing threads, which are easily searchable.

2 Likes

Well we could just implement the original proposal. I'd be OK with that.

Or drop the "+" requirement from name-matching if the concern is that some weird OS does not allow it.

But honestly, IMHO Swift should neither know nor care which file some code is declared in. All imports should be global to the module. A module should compile the same even if everything was copy-pasted into the same file. Namespaces (not files) should be used to create smaller scopes than modules.

Because what if some OS does not use "files" at all, as we know them? Files are just illusions after all.

I suggested using filenames because we already have a concept of file-based privacy scoping in Swift ("fileprivate").

1 Like

Am I overlooking something, or does this re-proposal (like the original proposal) completely ignore the "security" implications of this change?

In the example given above, anyone can write an extension that sets bar arbitrarily, just by meeting the meaningless Extra Restriction 1. In other words, the access to bar is basically internal, so the typeprivate didn't do anything.

Side note: What is Quxable in the above example? Is it a protocol? Is it a random symbol taken from the file name? What about other kinds of extensions — what is the actual syntax being proposed for the extension site here?

4 Likes

This is a strong -1 for me.

We have traveled down the road of modifying access control in Swift multiple times now, and I don't think anyone in the community is entirely happy with the result. I think we should leave it as-is and focus our energy on something else.

3 Likes

I won't delve into the access control since I'll get seriously passionate, but there are a few things I'd like to say, at least for future reference too.

Swift's stance is strictly to be non-dialectal (which also bleeds into strong source-compatibility requirement). So compiler flag that enables/disables officialized feature is most definitely a no go.

Let's not get hypothetical. The current access control already assumes an existence of files. If we're to support such an OS, the very first question would be how much ubuntu code can we reuse... *cough* *cough* I mean, one of the very first questions would be, what do we do with fileprivate?

2 Likes

I agree with the premise that I wish something like typeprivate existed. I wish there was an easy way to expose internal functionality only to subclasses or extensions on the type elsewhere in the module.

However. I do not believe this is the correct approach. It leaves open a huge hole of "what does it mean outside the module?". For example, UIKit exposes public methods (UIGestureRecognizerSubclass.h) that are meant to only be used by custom subclasses of UIGestureRecognizer; this seems like a prime candidate for something like typeprivate. How would something like that work?

At a higher level, I'm not interested in adding another one-off access control level without a deeper unifying concept. Back at the end of The Great Access Control Debate of 2017, @Erica_Sadun and Jeffrey Bergier came up with a proposal to allow flexible access scoping. It would allow for the creation of things like typeprivate or even friend without requiring those notions to be baked into the compiler. I would love to see something like this considered before we think about special casing yet another access control level.

That has quite a nice ring to it, and it reflects how much contention there was about the subject matter.

Are you sure about your basic premise here? I think in ancient Swift, an extension in the same file as its type's primary definition could not see the private members from that primary definition. The members had to be at least fileprivate for same-file extensions to see them. But that was changed in Swift 4 (3?) so private would work. So this idea is unnecessary right off the bat unless you meant something different (or some kind of subtle unhandled case).

It is the very arbitrary-ness of a file that makes it such a good way to define an access control boundary.

The problem with something like typeprivate is that, if the enclosing type doesn’t quite encompass the code that ought to have access to the declaration, you either need to move declarations into/out of the type to grant/restrict access, or you need to abandon typeprivate. If you do the former, you’re contorting the design of your types to fit into the access control system, which can have some unfortunate knock-on effects. If you do the latter, then the feature is useless to you.

Files don’t have that problem—other than access control (and the tautological purpose of #file and friends), they don’t have any other function. They’re vaguely used for code organization, but you usually want one file per “concern” or “function”, which is basically where you want access control boundaries anyway. The result is a feature that’s almost as flexible as C++ friend, but way less complicated.

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.

9 Likes
Terms of Service

Privacy Policy

Cookie Policy