[swift-evolution] [Idea] How to eliminate 'optional' protocol requirements

Dave Abrahams dabrahams at apple.com
Sun Apr 10 13:46:57 CDT 2016


on Sun Apr 10 2016, Dietmar Planitzer <swift-evolution at swift.org> wrote:

> I’m not sure whether you’ve read the conclusion of my mail since
> you’ve only commented on the introductory part. In the conclusion I
> wrote that a possible approach for the replacement of ObjC-style
> optional protocol methods would be:
>
> 1) the default implementation of a protocol method must be defined in
> the protocol (so just like in native Swift protocols today).

? They can and must be defined in protocol extensions today.

>
> 2) we add a way for a protocol provider to check whether the protocol
> adopter has provided an “override” of the default method.

I object to this part.

> 3) we improve the Xcode interface generator so that it clearly shows
> whether a protocol method comes with a default or whether it doesn’t.

Obvious goodness, long overdue.

> (1) should address your main concern since it would guarantee that the
> protocol provider is always able to call the protocol method without
> having to do any checks. (2) would address the main concern of
> protocol providers who need to guarantee that the protocol using type
> achieves a certain minimum speed and does not use more than a certain
> amount of memory for its internal book-keeping.

I don't how (2) can possibly help with that.
>
> (3) is important because it would fix one of the many aspects that
> make Swift protocols confusing for people who are new to the language.
>
> Finally, let me restate that the goal should really be that we
> completely remove the syntactical differences between @objc and native
> Swift protocols. There should be one concept of protocol in Swift and
> we should be able to cover the use cases of formal and informal ObjC
> Protocols with them. The use case of formal protocols is already
> covered today. The use case of informal protocols could be covered
> with the approach above.
>
> So is this an approach that would be acceptable to you?
>
> Regards,
>
> Dietmar Planitzer
>
>> 
>> On Apr 10, 2016, at 10:29, Dave Abrahams via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>> 
>> on Fri Apr 08 2016, Dietmar Planitzer <swift-evolution at swift.org> wrote:
>> 
>>> The biggest missing part with this model is that we are still not able
>>> to enable macro-level optimizations in the delegating type by checking
>>> whether the delegate does provide his own implementation of an
>>> optional method or doesn’t. However, this is an important advantage of
>>> the ObjC model that we should not lose.
>>> 
>>> Maybe it’s time to take a big step back and ignore the question of how
>>> to implement things for a moment and to instead focus on the question
>>> of what the conceptual differences are between ObjC protocols with
>>> optional methods and Swift protocols with default
>>> implementations. There are two relevant viewpoints here:
>>> 
>>> 1) From the viewpoint of a protocol adaptor:
>>> 
>>> ObjC:
>>> 
>>> 1a) adopter may provide his own implementation of the protocol method,
>>> but he is no required to.
>>> 
>>> 1b) adopter can see in the protocol declaration for which methods he
>>> must provide an implementation. Those methods do not have the
>>> “optional” keyword in front of them while optional methods do.
>>> 
>>> Swift:
>>> 
>>> 1c) same as (1a).
>>> 
>>> 1d) opening a binary-only Swift file in Xcode with a protocol
>>> definition in it which contains methods with default implementations
>>> will not give any indication of which method has a default
>>> implementation and which doesn’t. It’s only possible to see a
>>> difference on the syntax level if you have access to the sources.
>> 
>> This visibility problem is something we aim to correct in Swift, but
>> that is a question of syntax, documentation, and “header” generation,
>> and really orthogonal to what's fundamental about “optional
>> requirements:” 
>> 
>> 1. The ability to “conform” to the protocol without a
>>   default implementation of the requirement have been provided
>>   anywhere.
>> 
>> 2. The ability to dynamically query whether a type actually provides the
>>   requirement.
>> 
>> Both of these “features,” IMO, are actually bugs.
>> 
>>> So from the viewpoint of the protocol adopter, there isn’t much of a
>>> difference. The only relevant difference is that its always possible
>>> in ObjC to tell whether a protocol method must be implemented by the
>>> adopter or whether a method already has a default behavior. We
>>> shouldn’t actually have to change anything on the syntax-level in
>>> Swift to fix this problem. It should be sufficient to improve the
>>> Swift interface generator in Xcode so that it gives an indication
>>> whether a protocol method has a default implementation or doesn’t. Eg
>>> if we want to ensure that the generated interface is valid syntax then
>>> we could do this:
>>> 
>>> protocol Foo {
>>> 
>>>   func void bar() -> Int /* has default */
>>> 
>>> }
>>> 
>>> or if we say that it is fine that the generated interface is not valid
>>> syntax (I think it already shows "= default” for function arguments
>>> with a default value which I don’t think is valid syntax), then we
>>> could do this:
>>> 
>>> protocol Foo {
>>> 
>>>   func void bar() -> Int {…}
>>> 
>>> }
>>> 
>>> Now on to the other side of the equation.
>>> 
>>> 2) From the viewpoint of the protocol provider (the person who defines
>>> the protocol and the type that will invoke the protocol methods):
>>> 
>>> ObjC:
>>> 
>>> 2a) provider has freedom in deciding where to put the default
>>> implementation and he can put the default implementation in a single
>>> place or spread it out if necessary over multiple places. So has the
>>> freedom to choose whatever makes the most sense for the problem at
>>> hand.
>> 
>> But freedom for protocol implementors reduces predictability for protocol
>> clients and adopters.
>> 
>>> 2b) provider can detect whether the adopter provides his own protocol
>>> method implementation without compromising the definition of the
>>> protocol (compromising here means making return values optional when
>>> they should not be optional based on the natural definition of the
>>> API). This enables the provider to implement macro-level optimizations
>>> (eg table view can understand whether fixed or variable row heights
>>> are desired).
>>> 
>>> Swift:
>>> 
>>> 2c) provider is forced to put the default implementation in a specific
>>> place.
>>> 
>>> 2d) provider has no way to detect whether the adopter has provided his
>>> own implementation of the protocol method.
>>> 
>>> I do think that (2a) would be nice to have but we can probably do
>>> without it if it helps us to make progress with this topic. However,
>>> the ability to detect whether a protocol adopter provides his own
>>> implementation of a protocol method which comes with a default is a
>>> useful and important feature which helps us in optimizing the
>>> implementation of types and which allows us to keep the API surface
>>> smaller than it would be without this ability. Just go and compare eg
>>> UITableView to the Android ListView / RecyclerView to see the
>>> consequences of not having that ability and how it inflates the API
>>> surface (and keep in mind that the Android equivalents provide a
>>> fraction of the UITableView functionality).
>>> 
>>> The important point about (2b) is actually that we are able to detect
>>> whether an “override” (I’ll just call this overriding for now) of the
>>> default implementation exists or does not exist. 
>> 
>> IMO the important point about (2b) is that it leads to protocol designs
>> that create work and complexity for clients of the protocol, and being
>> constrained to make your protocol work so that clients don't have to do
>> these kinds of checks is a Very Good Thing™.
>> 
>>> In ObjC we make this distinction by checking whether an implementation
>>> of the method exists at all. But we don’t have to do it that way. An
>>> alternative approach could be based on a check that sees whether the
>>> dispatch table of the delegate contains a pointer to the default
>>> implementation of the protocol method or to some other method. So
>>> conceptually what we want is an operation like this:
>>> 
>>> func void useDelegate(delegate: NSTableViewDelegate) {
>>> 
>>>   if has_override(delegate, tableView(_:, heightOfRow:)) { // ask the
>>> delegate how many rows it has // allocate the geometry cache // fill
>>> in the geometry cache by calling tableView(_:, heightForRow:) for each
>>> row } else { // nothing to do here } }
>>> 
>>> Which would get the job done but doesn’t look good. Maybe someone has
>>> a better idea of how the syntax such an operator could look.
>>> 
>>> So my point here is that what we care about is the ability to detect
>>> whether the adopter provides an implementation of a protocol method
>>> which comes with a default implementation. The point is not that Swift
>>> protocols should work the exact same way that ObjC protocols have been
>>> working under the hood. But I do think that we want to eventually get
>>> to a point where the @objc attribute disappears and that we get a
>>> truly unified language on the syntactical level. An approach where:
>>> 
>>> I) we accept that the default behavior of a protocol method has to be
>>> provided by the protocol itself
>>> 
>>> II) the language is extended with a mechanism that makes it possible
>>> for a protocol provider to detect whether the adopter has “overridden”
>>> the default implementation
>>> 
>>> III) we improve the Xcode Swift interface generator so that it gives a
>>> clear indication whether a protocol method does come with a default
>>> implementation
>>> 
>>> would give us all the relevant advantages of ObjC-style optional
>>> protocol methods and it should allow us to create a unified syntax
>>> where there is no longer a visible difference between an optional
>>> protocol method that was imported from ObjC and a native Swift
>>> protocol with default implementations.
>>> 
>>> Regards,
>>> 
>>> Dietmar Planitzer
>>> 
>>>> On Apr 7, 2016, at 17:12, Douglas Gregor via swift-evolution
>>>> <swift-evolution at swift.org> wrote:
>>>> 
>>>> Hi all,
>>>> 
>>>> Optional protocol requirements in Swift have the restriction that
>>>> they only work in @objc protocols, a topic that’s come up a number
>>>> of times. The start of these threads imply that optional
>>>> requirements should be available for all protocols in Swift. While
>>>> this direction is implementable, each time this is discussed there
>>>> is significant feedback that optional requirements are not a feature
>>>> we want in Swift. They overlap almost completely with default
>>>> implementations of protocol requirements, which is a more general
>>>> feature, and people seem to feel that designs based around default
>>>> implementations and refactoring of protocol hierarchies are overall
>>>> better.
>>>> The main concern with removing optional requirements from Swift is their impact on Cocoa: Objective-C protocols, especially for delegates and data sources, make heavy use of optional requirements. Moreover, there are no default implementations for any of these optional requirements: each caller effectively checks for the presence of the method explicitly, and implements its own logic if the method isn’t there.
>>>> 
>>>> A Non-Workable Solution: Import as optional property requirements One suggestion that’s come up to map an optional requirement to a property with optional type, were “nil” indicates that the requirement was not satisfied. For example,
>>>> 
>>>> @protocol NSTableViewDelegate @optional - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row; - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row; @end
>>>> 
>>>> currently comes in as
>>>> 
>>>> @objc protocol NSTableViewDelegate { optional func tableView(_: NSTableView, viewFor: NSTableColumn, row: Int) -> NSView?  optional func tableView(_: NSTableView, heightOfRow: Int) -> CGFloat }
>>>> 
>>>> would come in as:
>>>> 
>>>> @objc protocol NSTableViewDelegate { var tableView: ((NSTableView, viewFor: NSTableColumn, row: Int) -> NSView?)? { get } var tableView: ((NSTableView, heightOfRow: Int) -> CGFloat)? { get } }
>>>> 
>>>> with a default implementation of “nil” for each. However, this isn’t practical for a number of reasons:
>>>> 
>>>> a) We would end up overloading the property name “tableView” a couple dozen times, which doesn’t actually work.
>>>> 
>>>> b) You can no longer refer to the member with a compound name, e.g., “delegate.tableView(_:viewFor:row:)” no longer works, because the name of the property is “tableView”.
>>>> 
>>>> c) Implementers of the protocol now need to provide a read-only property that returns a closure. So instead of
>>>> 
>>>> class MyDelegate : NSTableViewDelegate { func tableView(_: NSTableView, viewFor: NSTableColumn, row: Int) -> NSView? { … } }
>>>> 
>>>> one would have to write something like
>>>> 
>>>> class MyDelegate : NSTableViewDelegate { var tableView: ((NSTableView, viewFor: NSTableColumn, row: Int) -> NSView?)? = { … except you can’t refer to self in here unless you make it lazy ...  } }
>>>> 
>>>> d) We’ve seriously considered eliminating argument labels on function types, because they’re a complexity in the type system that doesn’t serve much of a purpose.
>>>> 
>>>> One could perhaps work around (a), (b), and (d) by allowing compound (function-like) names like tableView(_:viewFor:row:) for properties, and work around (c) by allowing a method to satisfy the requirement for a read-only property, but at this point you’ve invented more language hacks than the existing @objc-only optional requirements. So, I don’t think there is a solution here.
>>>> 
>>>> Proposed Solution: Caller-side default implementations
>>>> 
>>>> Default implementations and optional requirements differ most on the caller side. For example, let’s use NSTableView delegate as it’s imported today:
>>>> 
>>>> func useDelegate(delegate: NSTableViewDelegate) { if let getView = delegate.tableView(_:viewFor:row:) { // since the requirement is optional, a reference to the method produces a value of optional function type // I can call getView here }
>>>> 
>>>>  if let getHeight = delegate.tableView(_:heightOfRow:) { // I can call getHeight here } }
>>>> 
>>>> With my proposal, we’d have some compiler-synthesized attribute (let’s call it @__caller_default_implementation) that gets places on Objective-C optional requirements when they get imported, e.g.,
>>>> 
>>>> @objc protocol NSTableViewDelegate { @__caller_default_implementation func tableView(_: NSTableView, viewFor: NSTableColumn, row: Int) -> NSView?  @__caller_default_implementation func tableView(_: NSTableView, heightOfRow: Int) -> CGFloat }
>>>> 
>>>> And “optional” disappears from the language. Now, there’s no optionality left, so our useDelegate example tries to just do correct calls:
>>>> 
>>>> func useDelegate(delegate: NSTableViewDelegate) -> NSView? { let view = delegate.tableView(tableView, viewFor: column, row: row) let height = delegate.tableView(tableView, heightOfRow: row) }
>>>> 
>>>> Of course, the code above will fail if the actual delegate doesn’t implement both methods. We need some kind of default implementation to fall back on in that case. I propose that the code above produce a compiler error on both lines *unless* there is a “default implementation” visible. So, to make the code above compile without error, one would have to add:
>>>> 
>>>> extension NSTableViewDelegate { @nonobjc func tableView(_: NSTableView, viewFor: NSTableColumn, row: Int) -> NSView? { return nil }
>>>> 
>>>>  @nonobjc func tableView(_: NSTableView, heightOfRow: Int) -> CGFloat { return 17 } }
>>>> 
>>>> Now, the useDelegate example compiles. If the actual delegate implements the optional requirement, we’ll use that implementation. Otherwise, the caller will use the default (Swift-only) implementation it sees. From an implementation standpoint, the compiler would effectively produce the following for the first of these calls:
>>>> 
>>>> if delegate.responds(to: #selector(NSTableViewDelegate.tableView(_:viewFor:row:))) { // call the @objc instance method with the selector tableView:viewForTableColumn:row: } else { // call the Swift-only implementation of tableView(_:viewFor:row:) in the protocol extension above }
>>>> 
>>>> There are a number of reasons why I like this approach:
>>>> 
>>>> 1) It eliminates the notion of ‘optional’ requirements from the language. For classes that are adopting the NSTableViewDelegate protocol, it is as if these requirements had default implementations.
>>>> 
>>>> 2) Only the callers to these requirements have to deal with the lack
>>>> of default implementations. This was already the case for optional
>>>> requirements, so it’s not an extra burden in principle, and it’s
>>>> generally going to be easier to write one defaulted implementation
>>>> than deal with it in several different places. Additionally, most of
>>>> these callers are probably in the Cocoa frameworks, not application
>>>> code, so the overall impact should be small.
>>>> 
>>>> Thoughts?
>>>> 
>>>> 	- Doug
>>>> 
>>>> _______________________________________________ swift-evolution
>>>> mailing list swift-evolution at swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> 
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>> 
>> -- 
>> Dave
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-- 
Dave



More information about the swift-evolution mailing list