[Proposal] Make optional protocol methods first class citizens

Protocol requirements with default (no-op) implementations already satisfy that design goal, no?

Kind of. If I may steelman* optional members for a moment…

I’d actually go a bit further: for at least *some* of the uses of optional protocol methods, default implementations are necessary but not sufficient; to really “Swift-ify” without *losing* functionality you also would need to do a much-more-involved redesign of the protocol methods.

As just one example, I *think* the following API sketch is *adequate* for a Swift-ified` `tableView:heightForRowAtIndexPath:`:

  /// Mocked-up, "Swifty" table-view-delegate protocol:
  protocol TableViewDelegate : class {

    /// Non-optional method, returning a "policy" enum that e.g. provides enough
    /// information to the table view to let it choose the appropriate layout strategy...
    func rowHeightPolicyForTableView(tableView: TableView) -> TableViewRowHeightPolicy
    
  }

  /// “How to calculate height” information for an entire table view.
  enum TableViewRowHeightPolicy {
  
    /// all rows have identical height, table-view can operate in the "fast"
    /// mode, like setting `tableView.rowHeight = $someHeight` w/o delegate.
    case Uniform(CGFloat)
  
    /// table view should use auto-layout to measure each cell automatically;
    /// like `tableView.rowHeight = UITableViewAutomaticDimension` w/o delegate.
    case Automatic
  
    /// get per-row height info from `RowHeightCalculator`; equivalent to your
    /// having a delegate that implements `tableView:heightForRowAtIndexPath:`.
    case Manual(RowHeightCalculator)
    
  }
  
  /// A dedicated “row-height-calculator” protocol. On the one hand, this could
  /// also be "just a closure", but promoting it into a protocol seems more
  /// future-proof (e.g. if additional calculation-options become supported later).
  ///
  /// Unfortunately, making this an object means having to think harder than
  /// before about ownership issues; this seems mildly thorny.
  protocol RowHeightCalculator : class {
  
    /// Returns the height-information for the given row; to *fully* recapture
    /// existing `UITableView` functionality we need to return more than `CGFloat`.
    func heightForRow(in tableView: UITableView, at indexPath: NSIndexPath) -> RowHeight
  
  }
  
  /// Height-calculation result for a single row.
  enum RowHeight {
  
    /// Tells the table view to use auto-layout to measure this row's cell;
    /// equivalent to *returning* `UITableViewAutomaticDimension`.
    case Automatic
  
    /// Tells the table view to use the specified height for this row.
    case Height(CGFloat)
  
  }

…and at least AIUI that’s a complete representation of the various row-height-related behaviors it’s possible arrive-at, today, via various combinations of “did you implement `tableView:heightForRowAtIndexPath:` or not?” and/or “are you setting/returning `UITableViewAutomaticDimension`?.

Note also that this is just the behavior for row-heights; to fully-cover today’s user-exposed functionality, you’d need to handle header-heights and footer-heights (which have similar methods/semantics), and probably also the estimated-height methods for rows, headers, and footers.

Pausing here for a moment, I think the above illustrates why the topic keeps coming up: although it’s certainly *possible* to get to the same place w/out optional protocol methods, it sometimes takes a bit of a design rethink to get there. The API surface also seems to get quite a bit larger; I think it’s fair to classify this as simply “making already-present complexity actually-visible”, but it’d still be bit of a shift.

For the actual `tableView:heightForRowAtIndexPath:` family I’m not sure what I think; I personally like a more-explicit approach as per the above, but once you start to include the header-and-footer heights and also the estimated height APIs, it gets to be a bit much; more so when you think about how this API looks with similar approaches for other currently-optional method “suites" (like highlight/selection management “suite", the menu/action “suite”, etc.).

To conclude, in many cases "optional methods => methods w/ reasonable default implementation” is all you need, but in *some* cases you would also need to design your API very differently to have equivalent functionality.

I assume patterns and idioms for such designs will evolve over time, but at present there’s not a lot of Swift-ified datasource/delegate-style APIs out there to use for inspiration.

···

On Apr 1, 2016, at 7:37 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Right, but “more is worse” when it comes to language design. Having a "more general" facility that greatly overlaps with a “more narrow” facility always makes us question whether it is worth the complexity to have both.

-Chris

···

On Apr 3, 2016, at 10:40 AM, Andrey Tarantsov <andrey@tarantsov.com> wrote:

Protocol requirements with default (no-op) implementations already satisfy that design goal, no?

Chris, as we've discussed in a thread that I think got forked from this one:

Yes, they do technically, but it would be nice to both:

1) make it an obvious documented part of the signature, possibly including the default return value

2) possibly make it less verbose by getting rid of the explicitly spelled out protocol extension

For readability and specifically in this case, I think it does make sense
IMHO.

···

On Sunday, April 3, 2016, Chris Lattner <clattner@apple.com> wrote:

On Apr 3, 2016, at 10:40 AM, Andrey Tarantsov <andrey@tarantsov.com > <javascript:_e(%7B%7D,'cvml','andrey@tarantsov.com');>> wrote:

Protocol requirements with default (no-op) implementations already satisfy
that design goal, no?

Chris, as we've discussed in a thread that I think got forked from this
one:

Yes, they do technically, but it would be nice to both:

1) make it an obvious documented part of the signature, possibly including
the default return value

2) possibly make it less verbose by getting rid of the explicitly spelled
out protocol extension

Right, but “more is worse” when it comes to language design. Having a
"more general" facility that greatly overlaps with a “more narrow” facility
always makes us question whether it is worth the complexity to have both.

-Chris

As the problem seems to be to eliminate having to write the extension with all its duplication, I'd prefer a more general solution instead of introducing the notion of an "optional" function: just make it possible to write default implementations inline in a protocol definition.

Documenting the optionality can be done in the doc comment, maybe with a new documentation keyword "default". Having "optional" in the code has no additional value over a comment because the method is not optional in the Obj-C sense and the proposal requires a default value. Therefore the presence of "optional" has essentially no effect at all and is better moved into a comment.

-Thorsten

···

Am 04.04.2016 um 00:13 schrieb Chris Lattner via swift-evolution <swift-evolution@swift.org>:

On Apr 3, 2016, at 10:40 AM, Andrey Tarantsov <andrey@tarantsov.com> wrote:

Protocol requirements with default (no-op) implementations already satisfy that design goal, no?

Chris, as we've discussed in a thread that I think got forked from this one:

Yes, they do technically, but it would be nice to both:

1) make it an obvious documented part of the signature, possibly including the default return value

2) possibly make it less verbose by getting rid of the explicitly spelled out protocol extension

Right, but “more is worse” when it comes to language design. Having a "more general" facility that greatly overlaps with a “more narrow” facility always makes us question whether it is worth the complexity to have both.

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Protocol requirements with default (no-op) implementations already satisfy that design goal, no?

Kind of. If I may steelman* optional members for a moment...

In cases where a default implementation would do, the default implementation will usually also be the behavior you want for a nil instance, but there's no convenient way to share logic between the two. For example, consider this:

  protocol UITableViewDelegate {
    ...
    func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
  }
  extension UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
      return tableView.rowHeight
    }
  }
  
  class UITableView {
    ...
    private func addRow(at indexPath: NSIndexPath) {
      ...
      cell.size.height = delegate?.tableView(self, heightForRowAtIndexPath: indexPath) ?? rowHeight
      ...
    }
    ...

You have to duplicate the default logic both in the default implementation and at the call site, but there is no convenient way to share it—the extension method can't call into an expression at some call site, and contrarily the call site can't invoke the default logic from the extension.

Interesting point.

If the method were optional, then optional chaining would solve this problem for us:

  protocol UITableViewDelegate {
    ...
    optional func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
  }
  
  class UITableView {
    ...
    private func addRow(at indexPath: NSIndexPath) {
      ...
      cell.size.height = delegate?.tableView?(self, heightForRowAtIndexPath: indexPath) ?? rowHeight
      ...
    }
    ...

This way, there is only one source of default behavior: the call site.

It’s “each" call site, not “the” call site. If there are multiple call sites, we’d presumably want to factor out the default behavior computation anyway.

I'm also concerned by the thought of just how many sub-protocols we might end up with. When I try to fully factor NSTableViewDelegate (as it currently exists in the headers), I end up with ten protocols:

  NSTableViewDelegate
    - tableView:willDisplayCell:forTableColumn:row:

  NSTableViewLayoutDelegate: NSTableViewDelegate
    - tableView:heightOfRow:

  NSTableViewRowSelectionDelegate: NSTableViewDelegate
    - tableView:shouldSelectRow:
    - selectionShouldChangeInTableView:
    - tableViewSelectionIsChanging:
    - tableViewSelectionDidChange:
    - tableView:shouldTrackCell:forTableColumn:row: (10.5)
    - tableView:selectionIndexesForProposedSelection: (10.5)

  NSTableViewTypeSelectDelegate: NSTableViewDelegate (10.5)
    - tableView:typeSelectStringForTableColumn:row:
    - tableView:nextTypeSelectMatchFromRow:toRow:forString:
    - tableView:shouldTypeSelectForEvent:withCurrentSearchString:

  NSTableViewToolTipDelegate: NSTableViewDelegate
    - tableView:toolTipForCell:rect:tableColumn:row:mouseLocation:

  NSTableViewColumnDelegate: NSTableViewDelegate
    - tableView:shouldEditTableColumn:row:
    - tableView:shouldSelectTableColumn:
    - tableView:mouseDownInHeaderOfTableColumn:
    - tableView:didClickTableColumn:
    - tableView:didDragTableColumn:
    - tableViewColumnDidMove:
    - tableViewColumnDidResize:
    - tableView:sizeToFitWidthOfColumn: (10.6)
    - tableView:shouldReorderColumn:toColumn: (10.6)

  NSTableViewCellExpansionDelegate: NSTableViewDelegate (10.5)
    - tableView:shouldShowCellExpansionForTableColumn:row:
  
  NSTableViewCustomCellDelegate: NSTableViewDelegate (10.5)
    - tableView:dataCellForTableColumn:row:
    - tableView:isGroupRow:

  NSTableViewCellViewDelegate: NSTableViewDelegate (10.7)
    - tableView:viewForTableColumn:row:

  NSTableViewRowViewDelegate: NSTableViewDelegate (10.7)
    - tableView:rowViewForRow:
    - tableView:didAddRowView:forRow:
    - tableView:didRemoveRowView:forRow:
    - tableView:rowActionsForRow:edge: (10.11)

Some of these are probably unnecessary; they could be merged into NSTableViewDelegate and given default implementations. But at least a few of them would be very much needed. Would users be able to navigate this mess? Would they discover the features tucked away in sub-protocols? I'm just not sure.

This is what concerns me most. Delegate protocols tend to grow large over time (for good reason), and having a large family of related protocols is hard to navigate.

  - Doug

···

On Apr 1, 2016, at 5:37 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

I understand the concern.

To me, the answer is clearly yes. The language cannot be considered in isolation from its use cases; imagine UIKit written in Swift.

You want the developers to be able to quickly understand which table view delegate methods they need to implement, and what the contract is (are cells editable or non-editable by default?).

We need the same thing for our own UIKit-style controls we're writing today (and we don't want to be limited to @objc types when doing that — those are particularly ill-suited for the return values of optional delegate methods; I often want a CGFloat? or a enum).

I don't see this as a separate faculty as much as a shorthand, similar to how T? is a shorthand for Optional<T>.

The general opinion on shorthands varies from “there should be exactly one way to do everything” Python-style to “the language should help the developer express themselves” Ruby/Perl-style.

I personally am firmly in the “Ruby camp” here, so I'm all for use case-specific shorthands for general facilities.

You, sir (together with your dream team), should probably pick an official stance on this matter. :-)

A.

···

On 4 Apr 2016 04:15 +0600, Yuval Tal<yuvalt@pblc.co>, wrote:

For readability and specifically in this case, I think it does make sense IMHO.

On Sunday, April 3, 2016, Chris Lattner<clattner@apple.com(mailto:clattner@apple.com)>wrote:
>
> > On Apr 3, 2016, at 10:40 AM, Andrey Tarantsov<andrey@tarantsov.com(javascript:_e(%7B%7D,'cvml','andrey@tarantsov.com');)>wrote:
> > > Protocol requirements with default (no-op) implementations already satisfy that design goal, no?
> > Chris, as we've discussed in a thread that I think got forked from this one:
> >
> > Yes, they do technically, but it would be nice to both:
> >
> > 1) make it an obvious documented part of the signature, possibly including the default return value
> >
> > 2) possibly make it less verbose by getting rid of the explicitly spelled out protocol extension
> Right, but “more is worse” when it comes to language design.Having a "more general" facility that greatly overlaps with a “more narrow” facility always makes us question whether it is worth the complexity to have both.
>
> -Chris

That’s an interesting idea. Although I still do not like the fact that the Swift model forces me to split up the implementation between the delegating type and the delegate whereas the ObjC model allows me to keep the default and non-default implementation in a single place, namely the type.

There are some additional questions that I think have not received a satisfactory answer yet:

a) the ObjC model makes it straight forward to ADD new functionality to a delegate without breaking existing apps. Eg the tableView(heightForRowAtIndexPath:) method was added in Mac OS X 10.4. Before that the NSTableView did not support variable row heights. The only change to the API that Apple had to introduce in order to allow apps to create tables with variable row heights was this one optional delegate method. App implements it -> table view stores a per-row height; app does not implement it -> table view uses a fixed row height which is set as a property on the table. And so existing apps would continue to run with the new AppKit version just fine.

How would this work in the Swift model?

b) the ObjC model allows for significant and in some cases important optimizations:

b1) NSTableView support fixed and variable row heights. Layout can be much simpler for fixed row heights and there is no need to store the per-row height in an internal cache. In the ObjC model the table view has an efficient means to detect whether the delegate wants fixed or variable row heights since all it needs to do is check whether the tableView(heightForRowAtIndexPath:) is implemented. If it is -> call it for every row and cache the row height and enable the variable row height layout code; otherwise -> do not cache per row heights and enable the fixed row height layout code. Note that the code that does the actual heavy duty work (the layout code) may be far remove from the code that calls the tableView(heightForRowAtIndexPath:) method. In the Swift model though, we would have to invoke the tableView(heightForRowAtIndexPath:) for every row and compare the return results before we could even decide if the delegate wants variable or fixed row heights.

b2) the ObjC model allows me to get the implementation of the optional delegate method which in turn allows me to completely remove dynamic dispatch from the performance critical path.

How would these things work in the Swift model?

Regards,

Dietmar Planitzer

···

On Apr 1, 2016, at 18:20, Rob Mayoff via swift-evolution <swift-evolution@swift.org> wrote:

        class UITableView {
                ...
                private func addRow(at indexPath: NSIndexPath) {
                        ...
                        cell.size.height = delegate?.tableView(self, heightForRowAtIndexPath: indexPath) ?? rowHeight
                        ...
                }
                ...

You need not duplicate the default logic:

private class DefaultDelegate: NSObject, UITableViewDelegate { }
private let defaultDelegate = DefaultDelegate()

public class UITableView {

    private func addRow(at indexPath: NSIndexPath) {
        ...
        cell.size.height = (delegate ?? defaultDelegate).tableView(self, heightForRowAtIndexPath: indexPath)
        ...
    }

}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Very good points Dietmar, but there is one more bit about default implementations in extensions that makes it worse to use than the old Objective-C model... the complex dispatch rules that can make the executed method type dependent and not instance dependent. We really need dynamic dispatch there to make it work.

[[iOS messageWithData:ideas] broadcast]

···

On 2 Apr 2016, at 02:50, Dietmar Planitzer via swift-evolution <swift-evolution@swift.org> wrote:

That’s an interesting idea. Although I still do not like the fact that the Swift model forces me to split up the implementation between the delegating type and the delegate whereas the ObjC model allows me to keep the default and non-default implementation in a single place, namely the type.

There are some additional questions that I think have not received a satisfactory answer yet:

a) the ObjC model makes it straight forward to ADD new functionality to a delegate without breaking existing apps. Eg the tableView(heightForRowAtIndexPath:) method was added in Mac OS X 10.4. Before that the NSTableView did not support variable row heights. The only change to the API that Apple had to introduce in order to allow apps to create tables with variable row heights was this one optional delegate method. App implements it -> table view stores a per-row height; app does not implement it -> table view uses a fixed row height which is set as a property on the table. And so existing apps would continue to run with the new AppKit version just fine.

How would this work in the Swift model?

b) the ObjC model allows for significant and in some cases important optimizations:

b1) NSTableView support fixed and variable row heights. Layout can be much simpler for fixed row heights and there is no need to store the per-row height in an internal cache. In the ObjC model the table view has an efficient means to detect whether the delegate wants fixed or variable row heights since all it needs to do is check whether the tableView(heightForRowAtIndexPath:) is implemented. If it is -> call it for every row and cache the row height and enable the variable row height layout code; otherwise -> do not cache per row heights and enable the fixed row height layout code. Note that the code that does the actual heavy duty work (the layout code) may be far remove from the code that calls the tableView(heightForRowAtIndexPath:) method. In the Swift model though, we would have to invoke the tableView(heightForRowAtIndexPath:) for every row and compare the return results before we could even decide if the delegate wants variable or fixed row heights.

b2) the ObjC model allows me to get the implementation of the optional delegate method which in turn allows me to completely remove dynamic dispatch from the performance critical path.

How would these things work in the Swift model?

Regards,

Dietmar Planitzer

On Apr 1, 2016, at 18:20, Rob Mayoff via swift-evolution <swift-evolution@swift.org> wrote:

       class UITableView {
               ...
               private func addRow(at indexPath: NSIndexPath) {
                       ...
                       cell.size.height = delegate?.tableView(self, heightForRowAtIndexPath: indexPath) ?? rowHeight
                       ...
               }
               ...

You need not duplicate the default logic:

private class DefaultDelegate: NSObject, UITableViewDelegate { }
private let defaultDelegate = DefaultDelegate()

public class UITableView {

   private func addRow(at indexPath: NSIndexPath) {
       ...
       cell.size.height = (delegate ?? defaultDelegate).tableView(self, heightForRowAtIndexPath: indexPath)
       ...
   }

}

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

As the problem seems to be to eliminate having to write the extension with all its duplication, I'd prefer a more general solution instead of introducing the notion of an "optional" function: just make it possible to write default implementations inline in a protocol definition.

I think we can consider it as a given that, at some point, we’ll be able to write default implementations inline in the protocol definition. It’s not there now because we never got around to implementing it.

Documenting the optionality can be done in the doc comment, maybe with a new documentation keyword "default". Having "optional" in the code has no additional value over a comment because the method is not optional in the Obj-C sense and the proposal requires a default value. Therefore the presence of "optional" has essentially no effect at all and is better moved into a comment.

I tend to agree. ‘optional’ and the ‘= value’ syntax are fairly heavyweight language mechanisms for what is effectively documentation.

  - Doug

···

On Apr 3, 2016, at 10:21 PM, Thorsten Seitz via swift-evolution <swift-evolution@swift.org> wrote:

-Thorsten

Am 04.04.2016 um 00:13 schrieb Chris Lattner via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>>:

On Apr 3, 2016, at 10:40 AM, Andrey Tarantsov <andrey@tarantsov.com <mailto:andrey@tarantsov.com>> wrote:

Protocol requirements with default (no-op) implementations already satisfy that design goal, no?

Chris, as we've discussed in a thread that I think got forked from this one:

Yes, they do technically, but it would be nice to both:

1) make it an obvious documented part of the signature, possibly including the default return value

2) possibly make it less verbose by getting rid of the explicitly spelled out protocol extension

Right, but “more is worse” when it comes to language design. Having a "more general" facility that greatly overlaps with a “more narrow” facility always makes us question whether it is worth the complexity to have both.

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

I'd prefer a more general solution instead of introducing the notion of an "optional" function: just make it possible to write default implementations inline in a protocol definition.

This would work, too. I guess there's no need for an “optional” keyword if the implementation is right there in the protocol declaration.

A.