Pre-Pitch: Public Types vs. Private Types


(Jeff Kelley) #1

There’s a pattern I’ve seen in a lot of Swift code that uses a private property to shadow a public one:

protocol FooProtocol {}
class Foo: FooProtocol {}

class Bar {
    private var _foo: Foo
    public var foo: FooProtocol {
        get { return _foo }
    }
}

You have some type Foo that you’re storing, modifying, etc. in your class, but you want consumers of your class to see it as FooProtocol (or perhaps a superclass, such as when using subclasses of UIView elements). So, you have private and public versions of the type, and you might make the public version a computed property. More advanced cases might convert between types—you could store a piece of data as an Int internally but need to expose it as a Double, for instance. What I’d like to pitch is a way for Swift to explicitly indicate when this is happening:

protocol FooProtocol {}
class Foo: FooProtocol {}

class Bar {
    publicType(FooProtocol) public private(set) var foo: Foo
}

I see this has having a few advantages:

  • You don’t have to track multiple properties. For each item, you need exactly one line to express both internal and external representations.
  • Internally, you don’t have to remember to use the private version when you need to use a method that’s only defined on the private type.
  • For types where there’s a conversion initializer (e.g. init(_ otherType: OtherType)), the compiler could automatically synthesize a computed property that returns the other type. For other types, we could potentially add syntax along the lines of a get {} method to do the conversion.

The code base I’m working with right now is rife with private variables prefixed with underscores, and I think this addition would help us clean it up a lot. So what do y’all think? I’m pretty open to suggestions when it comes to naming.


(Rex) #2

Reminds me of Kotlin's delegated properties. https://kotlinlang.org/docs/reference/delegated-properties.html

Very useful and I'm a fan of the by keyword. Potentially consider

public private(set) var foo: Foo by FooProtocol

or even just using as for this use case

public private(set) var foo: Foo as FooProtocol

?


(Jeff Kelley) #3

I like the as syntax there. I wonder if we would still need a way to designate public; would it make sense to be able to these two in different contexts?

public private(set) var foo: Foo as(public) FooProtocol
public private(set) var bar: Bar as(internal) BarProtocol

#4

First, I love the idea. I have code like this, too, and I'd love to clean it up.

I don't see a need to have as(<accessibility specifier>) over just plain as. I can't think of a case where I'd want internal access to the actual type, but not public. Just my opinion.


(Ethan Diamond) #5

Big +1 on the problem this is trying to solve from me.

We have this problem all the time with RxSwift. Internally we want an Observable type that exposes the ability to write to a stream, while we want consumers of the object to only have the ability to read from the stream.


(Matthew Johnson) #6

Keep in mind that Swift supports dynamic casts so if users can see the type / protocol used internally to write to the stream then they could downcast to that type and write to it. We might want to discuss solutions that do not have this weakness if the primary use case is to hide capabilities from users of the public property.


(Alejandro Martinez) #7

Another thing that could be solved with that old Property Behaviours pitch. I wish that was revived.


#8

The problem with this general approach is that it doesn't really solve this:

In general, the code that has access to the private version (typically the containing class implementation) needs to distinguish on a case by case basis which of the two versions it wants, so there need to be two kinds of access, so you do have to remember which you want.

I've always thought that this kind of problem is better solved by making the backing store of a property (potentially) explicit. Instead of this:

class Bar {
    private var _foo: Foo
    public var foo: FooProtocol {
        get { return _foo }
    }
}

you'd do something like this:

class Bar {
    public var foo: FooProtocol {
        var _foo: Foo
        get { return _foo }
    }
}

(with the getter being synthesized in easy cases). That would mean, I guess, that within the class implementation you could choose to reference the backing store directly as self.foo._foo, though I hadn't really thought that through.


(Jeff Kelley) #9

Can you give an example? I’ve never run into this being an issue in my code.

Coincidentally, the codebase I was talking about in the original post is doing this exact same thing.


#10

Object Pascal has a design in which instance variables and properties are declared separately. Properties can be defined to use either an instance variable or a method for the getter and/or the setter. I always thought it was an elegant system. Even supported subscripted properties which were backed by methods. The closest I've seen since I last used OP was the introduction of @property to ObjC 2.0.

I think your idea would be wonderful. The actual backing variable could even be considered private to the definition's scope, and thus hidden from the enclosing type. This would take care of your observation about being able to access self.foo._foo. I don't see a reason why that declaration needs to be considered visible.


(Jeff Kelley) #11

To me the ability of the enclosing class to use the private backing type is essential. As in the RxSwift example, you have a type you can write to inside of your class, but outside of the class, it’s read-only.


#12

For example, if you have a public foo setter that has side effects, you might want to set the value of _foo in the class's init without triggering the side effects.

As does Objective-C, which is why (I suspect) Swift doesn't. Making the backing store "visible" on all properties tends to encourage people to reference it directly even when there's no absolute reason to. That often ends up being a source of bugs, since there are two "setting" behaviors to choose between. I suspect it was an intentional Swift decision to make that problem go away.

But as @SlaunchaMan has said, there are cases where you do this anyway, by using a second, private property in Swift. That's arguably worse than the Obj-C solution, in those cases.

Yes, I was (vaguely) envisioning the interior _foo variable as being either fileprivate (accessible by the rest of the implementation of the class) or private (usable only by the accessors of the property).


(Ethan Diamond) #13

That's what we're doing on our side - dynamic casting. We chose it over the strategy used by op, but it doesn't get caught at compile time. It'd be great to get a miscast discovered by the compiler instead of crashing during run time.

Another thing we've tried is to have a protocol for our objects expose the Observable, while internally the object is a subclass of Observable. So something like:

class Object: ObjectProtocol {
   let observable: PublishSubject<String>()
}  

protocol ObjectProtocol {
    var observable: Observable<String> { get }
}

but it doesn't fulfill the protocol even though PublishSubject is a subclass of Observable.

I don't have a huge preference between op's suggestion and letting the protocol support subclasses and the like, but the pain point op brought up is definitely shared by my team.


(Jeff Kelley) #14

I’m glad others like this idea too! Now who wants to help me with the implementation so we could make a proposal? :sweat_smile:


#15

I mentioned that. However, the proposal here is closer to Object Pascal's, where the instance variable could be read from or written to directly, instead of going through an (overridable) accessor, as in ObjC.

As for access by the enclosing type, that was just a brain fart on my part. The whole point of the proposal is to allow the type to bypass its own getters and setters.

If I recall OP correctly, the properties were not available within the class that declared them. Accesses would always be on the instance variables, and the properties were purely a public interface. You'd still have 2 setters, but it's easier to prevent bugs when you know that internal code will never trigger the property accessors and outside code can only trigger the accessors.