[Idea] How to eliminate 'optional' protocol requirements


(Jon Hull) #1

Interesting proposal, but I wanted to mention a couple of potential issues off the top of my head. I know when I was using optional requirements in Objective C, I would often use the presence/lack of the method (not just whether it returned nil) in the logic of my program. I used the presence of a method as a way for the implementor of a delegate to naturally communicate whether they wanted a more advanced feature. The absence of the method itself is information which can be utilized, not just whether it returns nil, and I believe that is part of what people are asking for when they say they want optional methods in Swift.

Let me try to give a simplified example which I am not sure how you would work around in this proposal:

Let’s say there is a datasource protocol which optionally asks for an image associated with a particular piece of data (imagine a table or collection view type custom control). If the method is not implemented in the data source, then a different view is shown for each data point that doesn’t have a place for images at all. If the method is implemented, but returns nil, then a default image is used as a placeholder instead (in a view which has a place for images).

tl;dr: Optional methods are often used as customization points, and the methods, if implemented, may also have another meaning/use for nil.

Similarly, a different back-end implementation may be used in the case where an optional method is not implemented. Let’s say you have something like a tableview with an optional method giving rowHeights. If that method is unimplemented, it is possible to have a much more efficient layout algorithm… and in some cases, you may check for the existence of the optional method when the delegate is set, and swap out a different layout object based on which customizations are needed (and again nil might mean that a height/etc... should be automatically calculated). This is the ability I miss the most.

Not saying the proposal is unworkable, just wanted to add some food for thought. I know I really miss optional methods in Swift. In some areas Swift is a lot more powerful, but there are lots of things I used to do in Obj C that I haven’t figured out how to do in Swift yet (if they are even possible). I am kind of disturbed by the trend/desire to get rid of the smalltalk-ness, as opposed to finding new and safer ways to support that flexibility/expressiveness. I would really like to see swift deliver on it’s promise of being a more modern alternative to ObjC (which it isn’t yet, IMHO) instead of just a more modern alternative to C++/Java.

Thanks,
Jon

···

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


(Zachary Waldowski) #2

Table view semantics were discussed at length on a prior version of the
thread. That pattern is less than ideal; it essentially creates magic
behavior that's only described by documentation… or, worse, completely
forgotten about in documentation; something changing from version to
version of the framework; etc. I can not tell you how many times this
has tripped up members of my teams. Over in Cocoa proper, similar
behavior changes also arise (usually performance optimizations) from
whether or not you *override* a method, and it's even more confusing.

Such a practice should not be a cornerstone of a modern language; as
discussed in the prior thread, the different semantics of the measuring
methods (the current return values, as well as the implicit one from not
overriding) should be captured explicitly in an enum, with a clear
default return value. This is in line with the spirit of Swift. Your API
contract with the user is clear, and the introduction of default
implementations is versioned as a matter of public API.

It's interesting that you use the phrase "customization points". Our
text for teaching protocol extensions in Swift uses it heavily - that
*is* the behavior of protocols with default implementations in Swift
today. You delegate something out, but give it a default implementation
with logic you specify. That's a customization point, too.

Cheers!
Zachary Waldowski
zach@waldowski.me

···

On Fri, Apr 8, 2016, at 08:47 AM, Jonathan Hull via swift-evolution wrote:

Interesting proposal, but I wanted to mention a couple of potential
issues off the top of my head. I know when I was using optional
requirements in Objective C, I would often use the presence/lack of
the method (not just whether it returned nil) in the logic of my
program. I used the presence of a method as a way for the implementor
of a delegate to naturally communicate whether they wanted a more
advanced feature. The absence of the method itself is information
which can be utilized, not just whether it returns nil, and I believe
that is part of what people are asking for when they say they want
optional methods in Swift.

Let me try to give a simplified example which I am not sure how you
would work around in this proposal:

Let’s say there is a datasource protocol which optionally asks for an
image associated with a particular piece of data (imagine a table or
collection view type custom control). If the method is not
implemented in the data source, then a different view is shown for
each data point that doesn’t have a place for images at all. If the
method is implemented, but returns nil, then a default image is used
as a placeholder instead (in a view which has a place for images).

tl;dr: Optional methods are often used as customization points,
and the methods, if implemented, may also have another meaning/use
for nil.

Similarly, a different back-end implementation may be used in the case
where an optional method is not implemented. Let’s say you have
something like a tableview with an optional method giving rowHeights.
If that method is unimplemented, it is possible to have a much more
efficient layout algorithm… and in some cases, you may check for the
existence of the optional method when the delegate is set, and swap
out a different layout object based on which customizations are needed
(and again nil might mean that a height/etc... should be automatically
calculated). This is the ability I miss the most.

Not saying the proposal is unworkable, just wanted to add some food
for thought. I know I really miss optional methods in Swift. In some
areas Swift is a lot more powerful, but there are lots of things I
used to do in Obj C that I haven’t figured out how to do in Swift yet
(if they are even possible). I am kind of disturbed by the
trend/desire to get rid of the smalltalk-ness, as opposed to finding
new and safer ways to support that flexibility/expressiveness. I
would really like to see swift deliver on it’s promise of being a more
modern alternative to ObjC (which it isn’t yet, IMHO) instead of just
a more modern alternative to C++/Java.

Thanks,
Jon

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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Daniel Steinberg) #3

In this case, I would think you would move the optional method into a separate protocol. If the user implemented the method they would have to explicitly conform to this specific protocol in which case they would be treated differently.

Instead of having to check whether someone implemented an optional method in a protocol, you would check if they conformed to the protocol that now contains it. This is a much stronger contract that is more easily understood by both sides.

Daniel

···

On Apr 8, 2016, at 8:47 AM, Jonathan Hull via swift-evolution <swift-evolution@swift.org> wrote:

Interesting proposal, but I wanted to mention a couple of potential issues off the top of my head. I know when I was using optional requirements in Objective C, I would often use the presence/lack of the method (not just whether it returned nil) in the logic of my program. I used the presence of a method as a way for the implementor of a delegate to naturally communicate whether they wanted a more advanced feature. The absence of the method itself is information which can be utilized, not just whether it returns nil, and I believe that is part of what people are asking for when they say they want optional methods in Swift.

Let me try to give a simplified example which I am not sure how you would work around in this proposal:

Let’s say there is a datasource protocol which optionally asks for an image associated with a particular piece of data (imagine a table or collection view type custom control). If the method is not implemented in the data source, then a different view is shown for each data point that doesn’t have a place for images at all. If the method is implemented, but returns nil, then a default image is used as a placeholder instead (in a view which has a place for images).

tl;dr: Optional methods are often used as customization points, and the methods, if implemented, may also have another meaning/use for nil.

Similarly, a different back-end implementation may be used in the case where an optional method is not implemented. Let’s say you have something like a tableview with an optional method giving rowHeights. If that method is unimplemented, it is possible to have a much more efficient layout algorithm… and in some cases, you may check for the existence of the optional method when the delegate is set, and swap out a different layout object based on which customizations are needed (and again nil might mean that a height/etc... should be automatically calculated). This is the ability I miss the most.

Not saying the proposal is unworkable, just wanted to add some food for thought. I know I really miss optional methods in Swift. In some areas Swift is a lot more powerful, but there are lots of things I used to do in Obj C that I haven’t figured out how to do in Swift yet (if they are even possible). I am kind of disturbed by the trend/desire to get rid of the smalltalk-ness, as opposed to finding new and safer ways to support that flexibility/expressiveness. I would really like to see swift deliver on it’s promise of being a more modern alternative to ObjC (which it isn’t yet, IMHO) instead of just a more modern alternative to C++/Java.

Thanks,
Jon

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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


#4

Couldn’t agree more.

An telling example is NSFetchedResultsControllerDelegate.controller(_:didChangeObject:atIndexPath:forChangeType:newIndexPath:).

When this method is not implemented, the NSFetchedResultsController does not have to perform the heavy computations of individual changes in a Core Data fetch request, since nobody is listening to them.

In today’s Swift, this lazy behavior requires an extra configuration flag.

Gwendal Roué

···

Le 8 avr. 2016 à 14:47, Jonathan Hull via swift-evolution <swift-evolution@swift.org> a écrit :

Interesting proposal, but I wanted to mention a couple of potential issues off the top of my head. I know when I was using optional requirements in Objective C, I would often use the presence/lack of the method (not just whether it returned nil) in the logic of my program. I used the presence of a method as a way for the implementor of a delegate to naturally communicate whether they wanted a more advanced feature. The absence of the method itself is information which can be utilized, not just whether it returns nil, and I believe that is part of what people are asking for when they say they want optional methods in Swift.


(Andrew Bennett) #5

I agree, separate protocols is preferable. I think it needs some more
thought though, some UIKit protocols have several optional methods, I'm not
sure if a protocol for each would be very clean.

The other thing that needs consideration is whether it's possible to
automatically convert objc with optional methods to Swift without them. For
example, it would need to be compatible with checks for protocol
conformance, I'm not sure what edge cases will arise if the optional
methods are broken out into other protocols.

···

On Sunday, 10 April 2016, Daniel Steinberg via swift-evolution < swift-evolution@swift.org> wrote:

In this case, I would think you would move the optional method into a
separate protocol. If the user implemented the method they would have to
explicitly conform to this specific protocol in which case they would be
treated differently.

Instead of having to check whether someone implemented an optional method
in a protocol, you would check if they conformed to the protocol that now
contains it. This is a much stronger contract that is more easily
understood by both sides.

Daniel

On Apr 8, 2016, at 8:47 AM, Jonathan Hull via swift-evolution < > swift-evolution@swift.org > <javascript:_e(%7B%7D,'cvml','swift-evolution@swift.org');>> wrote:

Interesting proposal, but I wanted to mention a couple of potential issues
off the top of my head. I know when I was using optional requirements in
Objective C, I would often use the presence/lack of the method (not just
whether it returned nil) in the logic of my program. I used the presence
of a method as a way for the implementor of a delegate to naturally
communicate whether they wanted a more advanced feature. The absence of
the method itself is information which can be utilized, not just whether it
returns nil, and I believe that is part of what people are asking for when
they say they want optional methods in Swift.

Let me try to give a simplified example which I am not sure how you would
work around in this proposal:

Let’s say there is a datasource protocol which optionally asks for an
image associated with a particular piece of data (imagine a table or
collection view type custom control). If the method is not implemented in
the data source, then a different view is shown for each data point that
doesn’t have a place for images at all. If the method is implemented, but
returns nil, then a default image is used as a placeholder instead (in a
view which has a place for images).

tl;dr: Optional methods are often used as customization points, and the
methods, if implemented, may also have another meaning/use for nil.

Similarly, a different back-end implementation may be used in the case
where an optional method is not implemented. Let’s say you have something
like a tableview with an optional method giving rowHeights. If that method
is unimplemented, it is possible to have a much more efficient layout
algorithm… and in some cases, you may check for the existence of the
optional method when the delegate is set, and swap out a different layout
object based on which customizations are needed (and again nil might mean
that a height/etc... should be automatically calculated). This is the
ability I miss the most.

Not saying the proposal is unworkable, just wanted to add some food for
thought. I know I really miss optional methods in Swift. In some areas
Swift is a lot more powerful, but there are lots of things I used to do in
Obj C that I haven’t figured out how to do in Swift yet (if they are even
possible). I am kind of disturbed by the trend/desire to get rid of the
smalltalk-ness, as opposed to finding new and safer ways to support that
flexibility/expressiveness. I would really like to see swift deliver on
it’s promise of being a more modern alternative to ObjC (which it isn’t
yet, IMHO) instead of just a more modern alternative to C++/Java.

Thanks,
Jon

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@swift.org
<javascript:_e(%7B%7D,'cvml','swift-evolution@swift.org');>
https://lists.swift.org/mailman/listinfo/swift-evolution