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 
<http://thread.gmane.org/gmane.comp.lang.swift.devel/1316/focus=8804> of times 
<http://thread.gmane.org/gmane.comp.lang.swift.evolution/13347/focus=13480>. 
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
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to