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

Dietmar Planitzer dplanitzer at q.com
Sun Mar 6 16:57:41 CST 2016


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.


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.


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


Regards,

Dietmar Planitzer


> On Mar 4, 2016, at 13:24, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
> 
> 
>> On Mar 4, 2016, at 10:49 AM, John McCall via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>>> On Mar 4, 2016, at 10:09 AM, Shawn Erickson via swift-dev <swift-dev at swift.org> wrote:
>>> (Sorry I hate top posting but fighting with Google inbox to avoid it)
>>> 
>>> In delegation patterns it can be very helpful in terms of optimization (performance and memory) if the delegator can interrogate a delegate to know if certain delegation points are needed or not. I have code that has done this interrogation at delegate registration allowing potentially complex code paths and/or state maintenance to be avoided. It also could do impl caching to improve dispatch (did not support after registration "reconfiguring" delegates in this model purposely).
>>> 
>>> Under objective-c leveraging optional protocol methods and runtime checks for responds to those was one built-in way for that. It also could add boilerplate code to check before dispatch that was potentially error prone and cumbersome. ...hence why  I often did the check and configuration at registration in my code often avoiding peppering code with dispatch checks.
>>> 
>>> Anyway not attempting to state anything for against this question about optional requirements in swift protocols. ...a handful of reasonable patterns exist in swift that allows one to achieve the same thing in safer and likely more performant ways.
>> 
>> Folks, this is obviously a language design discussion.  Please do this sort of thing on swift-evolution.  I’ve moved swift-dev to BCC.
> 
> Even without language support, you can model optional protocol conformances as optional property requirements, with pretty much the exact same behavior on the user side that we give officially optional requirements:
> 
> protocol OptionalMethod {
>   var optionalMethod: Optional<() -> ()> { get }
> }
> 
> -Joe
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution



More information about the swift-evolution mailing list