[swift-evolution] [swift-dev] Is there an underlying reason why optional protocol requirements need @objc?

Joe Groff jgroff at apple.com
Mon Mar 7 10:37:29 CST 2016


> On Mar 6, 2016, at 2:57 PM, Dietmar Planitzer <dplanitzer at q.com> wrote:
> 
> I’ve played around with your idea a bit and I have some questions (please see below). Here is the code that I wrote up in playground (but I also ran all experiments outside the playground):
> 
> protocol Delegate {
>    var optionalMethod: Optional<() -> Int> { get }
> }
> 
> 
> class Foo {
> 
>    var delegate : Delegate?
> 
>    func doIt() {
>        if let d = delegate {
>            let i: Int = d.optionalMethod?() ?? 0
> 
>            print(i)
>        }
>    }
> }
> 
> 
> class Bar : Delegate {
> 
>    init() {
> //        optionalMethod = { self.p }   [A]
>    }
> 
>    deinit {
>        print("dead")
>    }
> 
>    var p = 1
> 
> //    var optionalMethod = Optional({ return 1 })
> 
>    var optionalMethod: Optional<() -> Int> = nil
> }
> 
> 
> class Bar1 : Bar {
> 
> //    override var optionalMethod: Optional<() -> Int> = { 1 } 
> 
> }
> 
> 
> do {
>    let f = Foo()
>    f.delegate = Bar()
>    f.doIt()
> }
> 
> 
> Assuming that this is what you had in mind as a replacement for optional protocol conformances, here are some questions:
> 
> 1) How would forward compatibility work with this approach? Eg we may create a class or struct as part of our v1 framework API and the class/struct may support a delegate protocol with a non-optional method. But based on feedback from the field we want to change the non-optional delegate method to an optional one in v2. The goal is to create a new version of the framework which will not break existing apps which were built against the v1 API. To me it looks like that I could not do this with your approach without breaking existing apps since the v1 protocol definition:
> 
> protocol Delegate {
>    var foo: Optional<() -> Int> { get }
> }
> 
> would change to this:
> 
> protocol Delegate {
>    var foo: Int { get }
> }
> 
> which implies that the call site needs to be different. The same problem exists the other way around: an optional requirement may change to a non-optional requirement. The extra complication here is that the delegating entity needs to be able to treat a method which is declared as non-optional in the protocol, as effectively optional at runtime in order to provide backward compatibility until the optionality can be phased out for good.

You can't resiliently change a required protocol requirement to an optional one, since this would break existing clients of the protocol that assume the existence of the method. You could at best add a default implementation.

> 
> 2) The example above shows class Bar, which adopts the delegate protocol and class Bar1 which subclasses Bar. Now Bar1 wants to override the “optionalMethod” in order to return a different value. But overriding doesn’t work because “optionalMethod” in Bar is now a stored property. To make this more concrete, assume that we are developing a media app for a mobile device and that the UI of this app is organized into tabs. Eg one tab for videos, another one for things that you shared, another one for folders, etc. All those pages have the same underlying principle in common: set up a db query, run the query asynchronously, wait for the query result while managing a progress spinner and finally post-process the results and show them in a table view. One of the things we want to support is drag & drop. The drag & drop delegation methods are all optional conformances and our common base view controller provides a generic implementation which makes sense for 90% of all pages. It’s just 1 or 2 pages that want to do drag & drop a bit differently and thus the view controller subclasses for those tabs should be able to just override the inherited d & d methods (the optional methods).
> 
> In your model, the only way I see this kind of work is when I do [A] (see code above) and the subclass would store different closures in the property. But this then introduces other issues.
> 
> 
> Here are my thoughts and observations on this:
> 
> a) I have a problem with the inconsistency that this approach introduces when you compare how to adopt a non-optional requirement vs an optional requirement:
> 
> protocol Delegate {
>   var requiredProperty: Int { get}
> }
> 
> 
> class Foo : Delegate {
> 
>   var requiredProperty = 1
> 
> }
> 
> So things are simple and intuitive if the method / property is non-optional. But if it is optional then I need to write this:
> 
> protocol Delegate {
>   var optionalProperty: Optional<() -> Int> { get}
> }
> 
> 
> class Foo : Delegate {
> 
>   var optionalProperty = Optional({1})
> 
> }
> 
> or this:
> 
> class Foo : Delegate {
> 
>   var optionalProperty:  Optional<() -> Int>  =  {1}
> 
> }
> 
> I really do not like that we put the burden of adopting optional methods on the adopter rather than the implementor of the delegating class. For every hour that is spent on implementing the delegating class, potentially hundreds and more hours will be spent writing code that adopts the delegate (think framework provided delegate protocols). So it clearly makes more sense to put the burden on the implementor of the delegating class rather than the individual adopters.

In principle it could be possible to satisfy get-only property requirements with `func` declarations, and vice versa.

> 
> b) the solution doesn’t scale well beyond trivial code because you can not simply write this:
> 
> class Foo : Delegate {
> 
>    var value = 1
> 
>    var optionalMethod = Optional({ value })
> }
> 
> because “self” in the closure does not refer to Foo. But even if it would, you would have to prefix “value” with “self” because it’s now inside a closure rather than a regular method / property body. Now one possible workaround for this would be this:
> 
> class Foo : Delegate {
> 
>   init() {
>      optionalMethod = { self.value}
>   }
> }
> 
> which however puts code that has nothing got to do with initialization inside the initializer.
> 
> 
> c) it makes it easy to create retain cycles because the optional method is now a closure. The adopter object has a strong reference to the closure. If the closure now captures the adopter’s  self, and we forget to mark the capture as unowned, then we’ve created a retain cycle and the adopter object won’t get freed. You can uncomment [A] in the example above to see this problem.
> 
> 
> d) so given (b) and (c) maybe we can rewrite the optional method like this:
> 
> class Foo : Delegate {
> 
>   init() {
>      optionalMethod = optionalMethodImpl
>   }
> 
> 
>   var optionalMethod: Optional<() -> Int> = nil
> 
>   func optionalMethodImpl() -> Int {
>      return 1
>   }
> }
> 
> which fixes (b) but apparently doesn’t fix (c) since the Foo object still does not get deallocated.
> 
> 
> Overall I prefer ObjC’s solution for optional protocol requirements over this because it would:
> 
> I) allow us to ensure that we can provide forward compatibility exactly the way we need it without compromising performance or safety
> 
> II) give us syntax that is easy to understand for the language user, is consistent with non-optional requirements and does not force us to compromise the design of our APIs
> 
> III) because of (II) consistency between fulfilling non-optional and optional requirements, it is less likely to write buggy code and refactoring from optional to non-optional and the other way around is less time consuming and safer

Fair points.

-Joe


More information about the swift-evolution mailing list