[Idea] How to eliminate 'optional' protocol requirements


(Douglas Gregor) #1

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


Extending declaration names for closures
(David Waite) #2

<snip>

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
}

If I understand correctly:

1. Optional methods in protocols imported from objective C gain a special flag when imported as Swift methods
2. Such methods can be unimplemented (such that the message will fail, or responds(to:) returns false) in objective-c code and bridged swift instances.
3. To implement that protocol in swift, you must have implementations of every protocol method, including optional ones
4. This means that there may be manual work around bridging protocols with optional messages into swift.
5. If the method implementation is marked @nonobjc it will fulfill the swift requirement that there be a method implementation, but that implementation will not be exposed to Objective-C
6. Swift code can call such a @nonobjc implementation, while Objective-C code will consider there to be nothing there.
7 An implementation that is only be applied to the swift variant is possibly fine in extensions while generates a warning within a swift class, or perhaps requires an explicit @nonobjc attribute.

I like it, except for the requirement for a manual implementation in Swift before you can support the protocol.

I looked at whether the protocol members might be implemented by returning the nil/zero/false value you would get if you sent the message to nil.

Cursory search quickly hit NSAccessibilityElement, which has an optional “accessibilityIdentifier” method returning a non-nil NSString. I suspect that the method also requires the string to be non-empty. Thus, a default implementation that represents the right thing to represent in a generated default implementation would likely be brittle.

I could also see an imported protocol where *any* default implementation of the optional method would not meet the requirements of an actual implementation of the method (not being versed in this particular interface, I’ll just straw man that the identifier is required to be unique within an application)

Thus I wonder if there may be some other way to support the idea of two distinct protocols, the protocol as defined in Objective C, and the protocol as defined in Swift.

Options that sprang to mind:
- the methods which return optional values have a default implementation that returns nil. Methods which return non-optional values will have the Swift protocol modified to return an Optional value, which they will do by default. So for example, Still on NSAccessibilityElement,

  -(BOOL)isAccessibilityFocused

would be imported as

  func isAccessibilityFocused() -> Bool?

with a default implementation returning nil. To actually implement the objective C protocol’s optional method, you must implement the version with the correct nullability variant, so in this case:

     @objc func isAccessiblityFocused() -> Bool { return focused }

(Of course, this means that a non-optional result value would need to satisfy an optional result valued variant in a protocol)

- similar to the above, but rather than overriding result values to support a default variant, overload ErrorType. Imported variants which throw will by default throw a bridging-specific ErrorType when called from Swift. Optional methods which do not throw will have a throwing variant generated on the Swift side.

Again similar to the above, to satisfy the objective-C protocol requirement your implementation would need to be non-throwing.

I like this better in terms of capturing to the best of ones ability the ‘spirit’ of optional methods and behavior in swift. However, this seems like it will result in more deviation between the Swift and Objective-C protocol method signatures.

Comments?

-DW

···

On Apr 7, 2016, at 6:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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


(Goffredo Marocchi) #3

Proposal sounds nice, but shouldn't it go hand in hand with the review of dispatching rules for protocol extensions (i.e.: dynamic dispatch by default unless overridden by a user declaration/annotation or when the compiler is sure no side effects will occur... ProtocolA and InstanceAImplementingProtocolA must behave the same when calling a method)?
In a type safe language, the lack of safety current complex dispatching rules bring seems odd not to address :/. Sorry for the aside rant.

[[iOS messageWithData:ideas] broadcast]

···

On 8 Apr 2016, at 01:12, Douglas Gregor via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Shawn Erickson) #4

I want to reiterate that I have objective-c code, others have objc code,
and the cocoa, etc. frameworks have code that depend on optional protocol
for things like (but not limited to) delegates. This is of course obvious
but what seems to get lost in the discussion is that you can't always
replace the non-existence of an implementation of an optional protocol
method with a default implementation.

I have code that probes a delegate when registered and based on the what
subset of the optional protocol methods it handles configures its runtime
state to optimize itself to that reality. For example it may avoid
allocating and maintaining potentially complex state if one or more methods
are not implemented by the delegate (since no one is interested in it). If
we just blindly provide default implementation for optional methods then
this optimization couldn't take place.

I know others - including I believe Apple framework code - do similar
optimizations based on what methods an object implements.

I think we should maintain the optional concept in support of bridging
existing objc code into swift (confined to @objc)... unless a way to bridge
things can be defined that avoids the loss of optimization potential I
outlined above.

Optional protocols don't need to be expanded into Swift itself since I
believe alternate methods and patterns exists to solve the same type of
need.

-Shawn

···

On Thu, Apr 7, 2016 at 5:12 PM Douglas Gregor via swift-evolution < swift-evolution@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
<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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dietmar Planitzer) #5

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.

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.

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


(Brent Royal-Gordon) #6

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 }
}

Okay, but NSTableViewDelegate is being imported from Objective-C. Suppose it doesn't ship with an extension that provides default values. What then? Are you unable to import AppKit? Do you have to write the extension yourself? (Even if you don't need it?)

And if you want to implement the exact semantics of `NSTableView`, including the fact that a table view whose delegate does not implement `tableView(_:heightOfRow:)` uses a fast path, what do you do?

* * *

Personally, I'm coming around to permitting optional methods in all types. Consider, for instance, WKWebView, which has a couple pairs like this:

  var canGoForward: Bool { get }
  func goForward()

You shouldn't be calling `goForward()` except on a WKWebView which `canGoForward`. So why are these pieces of information not conveyed together? Wouldn't it be better if WKWebView simply offered:

  optional func goForward()

And you couldn't call `goForward()` without checking or asserting that you're allowed to go forward at the moment?

Obviously that would require that you be able to *change* whether a given method was permitted or not based on circumstances. That would be a substantial expansion of the current `optional` feature, and overall, the whole thing simply doesn't seem important enough to prioritize.

But fundamentally, I don't think there's anything less coherent about saying "this member may not always exist" with an optional member than there is about saying "this property's value may not always exist" with an Optional value. "Subtypes may or may not support this thing, but if they do, this is what it'll look like" is a perfectly sensible thing to want from a type.

(Although we'll need to find a different name than "optional", that's for sure.)

···

--
Brent Royal-Gordon
Architechies


(Joe Groff) #7

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution. It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes, and your proposed solution makes this incredibly obscure and magical.

-Joe

···

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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.


Extending declaration names for closures
(Shawn Erickson) #8

Thanks for the excellent writeup and thought on this.

I strongly believe - as a protocol adaptor - that having the ability to
know if a default implementation is provided in the interface I see. It
would also be helpful if documentation (header docs) about the
characteristics of the default implement would be visible is so provided by
author. This wild help guide me in understanding if I need to bother with
overriding the default.

I also strongly believe - as a protocol provider - being able to know if an
adaptor has overridden the default implementation can be helpful in how I
able to implement and optimize my code as well as how I can define my
protocol (keeping it clean).

Note multiple default implementation could exist depending on scoping done
by where clauses against associated types, right?
That adds some complexity to this but it seems like it could be expressed
at a minimum via header docs exposed to adopters.

-Shawn

···

On Fri, Apr 8, 2016 at 11:04 PM Dietmar Planitzer via swift-evolution < swift-evolution@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.

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.

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. 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@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@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


(Dave Abrahams) #9

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™.

···

on Fri Apr 08 2016, Dietmar Planitzer <swift-evolution@swift.org> wrote:

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@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@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

--
Dave


(David Waite) #10

I think we have the following asks:
- support methods on a protocol that may or may not be implemented by a confirming type
- support statically (per type) detection of whether one of these methods have been implemented so that you can have consistent behavior while interacting with a particular type
- static methods may or may not be attractive to implement via additional subtype protocol conformance (depending on how many independent optional methods exist)
- support dynamically (based on internal logic and state) support a particular method, for instance with UI event dispatching
- Ability to determine if a method is available on a type outside of calling it to detect default functionality
- compatibility (where appropriate) with existing objc protocols with optional methods

Obviously, optional as it exists today only solves some of these asks. Some of these may not deserve to be (or be suitably improved upon by being) language features. Some of these are possible today in a form through Objective C interop, but this are not available on other platforms

-DW

···

Sent with my Thumbs

On Apr 11, 2016, at 11:15 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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.

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution. It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes, and your proposed solution makes this incredibly obscure and magical.

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


(Matthew Johnson) #11

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.

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution. It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes, and your proposed solution makes this incredibly obscure and magical.

Do you have the same thought for optional closure properties? If so and heightForRow was an optional closure property it would satisfy all use cases elegantly. It could have a default implementation that returns nil. When non-uniform heights are required a normal method implementation can be provided. Delegates that have uniform row heights some of the time, but not all of the time, would also be supported by implementing the property.

If we decide to favor this approach it would be really nice to be able to import Cocoa delegate protocols this way. Is that something that might be feasible?

···

Sent from my iPad

On Apr 11, 2016, at 12:15 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

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


(Douglas Gregor) #12

I want to reiterate that I have objective-c code, others have objc code, and the cocoa, etc. frameworks have code that depend on optional protocol for things like (but not limited to) delegates. This is of course obvious but what seems to get lost in the discussion is that you can't always replace the non-existence of an implementation of an optional protocol method with a default implementation.

I have code that probes a delegate when registered and based on the what subset of the optional protocol methods it handles configures its runtime state to optimize itself to that reality. For example it may avoid allocating and maintaining potentially complex state if one or more methods are not implemented by the delegate (since no one is interested in it). If we just blindly provide default implementation for optional methods then this optimization couldn't take place.

I know others - including I believe Apple framework code - do similar optimizations based on what methods an object implements.

Just to be very clear (which I think my initial post wasn’t) my proposal does *not* break this optimization when a Swift class is conforming to an @objc protocol: even if a default is present, it won’t be visible to the Objective-C runtime at all.

My proposal *does* make it significantly harder to implement a check for “did the type implement this method?”, because one will effectively have to use -respondsToSelector:. For Cocoa-defined delegates, that doesn’t matter at all: apps generally implement requirements of delegates/data sources, but almost never go through the protocol to use those methods/properties. It’s the frameworks that do the calling, and of course they’re already using -respondsToSelector: checks.

The main effect is in Swift code that uses @objc optionals and tests for the absence of an implementation to perform some optimization. The tenor of the previous thread seems to indicate that this probably isn’t common, because there are probably better ways to model these cases in Swift—whether it’s with multiple protocols or something that specifically describes the policy (e.g., http://thread.gmane.org/gmane.comp.lang.swift.evolution/13347/focus=13480).

I think we should maintain the optional concept in support of bridging existing objc code into swift (confined to @objc)... unless a way to bridge things can be defined that avoids the loss of optimization potential I outlined above.

The direction I’m trying to go is not to have half of a feature—something that seems like it should be general, but is tied to @objc—in the language. We get a very large number of requests to make “optional” work for Swift protocols, because it’s a confusing limitation and there is a ton of overlap with default implementations. It would be far better to remove the feature.

Optional protocols don't need to be expanded into Swift itself since I believe alternate methods and patterns exists to solve the same type of need.

Given that you don’t feel that optional requirements need to work in Swift-only protocols, and what I’ve said above about compatibility with Cocoa, do you still think we need to keep ‘@objc optional’ as a notion in the language?

  - Doug

···

On Apr 8, 2016, at 8:53 AM, Shawn Erickson <shawnce@gmail.com> wrote:

-Shawn

On Thu, Apr 7, 2016 at 5:12 PM Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <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
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Douglas Gregor) #13

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.

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution.

I haven’t seen these come up in any discussion that wasn’t about mapping Objective-C optional requirements to something else in Swift. What other use cases are you envisioning?

It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes,

Most of the requests I see for this feature are of the form “this works for @objc protocols, so it should work everywhere,” and most of the push-back I’ve seen against removing ‘optional’ is a concern over interaction with Cocoa. I haven’t gotten the sense that optional requirements are considered to be the best design for any particular task in Swift.

and your proposed solution makes this incredibly obscure and magical.

That’s fair. The mechanism I’m talking about *is* a bit hard to explain—we would need to rely on the diagnostic for cases where one tries to call a method that is caller-defaulted from Swift code, e.g.,

  error: method ‘foo(bar:wibble:)’ may not be implemented by the adopting class; add a default implementation via an extension to protocol ‘Foo'

This would only affect optional requirements of protocols imported from Objective-C. My hypothesis is that those just aren’t used in Swift app code.

  - Doug

···

On Apr 11, 2016, at 10:15 AM, Joe Groff <jgroff@apple.com> wrote:

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:


(Dietmar Planitzer) #14

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).

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

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.

(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.

(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@swift.org> wrote:

on Fri Apr 08 2016, Dietmar Planitzer <swift-evolution@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@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@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

--
Dave

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


(David Sweeris) #15

What about this? I’m unfamiliar with the details of objc-swift interoperability, so I’m not actually sure it could work this way, nor am I certain that I’m not just “rephrasing” something that’s already been suggested.
protocol SomeProtocol {
    //Non-optional stuff goes here
}
extension SomeProtocol {
    //Optional stuff goes here
    func blah() {
        //Default implementation goes here
    }
}

protocol SomeSubProtocol : SomeProtocol {
    //The same optional stuff goes here again
    func blah()
}

func foo<T : SomeProtocol> (x:T) {
    if let x = x as? SomeSubProtocol {
        x.blah() //Is custom implementation because we know x is SomeOtherProtocol
    } else {
        x.blah() //Is default implementation because we know it's not
    }
    ...
}

(It seems like there ought to be a less repetitive way to do the branch. The problem is that any expression would necessarily have to evaluate to two different types, depending on whether T conforms to the sub protocol.)

Anyway, does that correctly model the relationship between optional requirements and their protocols? I’ve used optional protocol requirements just enough to use UIKit and such, but I find the whole self-contradictional "optional requirement” thing confusing enough that I’ve never really felt like I understand them. There’s a fair chance that I’m missing their point.

If that is the correct model, is there a way to automatically create all the objc “subprotocols" as they’re being imported into swift? If so, would it be beneficial to maybe “nest” the protocols to prevent UIKit from declaring 50 million “top level” protocols?
protocol SomeProtocol {
    protocol SomeOtherProtocol { // the " : SomeProtocol" part is implied
        func blah() -> Int
    }
    //Non-optional stuff goes here
}
extension SomeProtocol {
    //Optional stuff goes here
    func blah() -> Int {
        return 5
    }
}
func blah<T : SomeProtocol> (x:T) -> Int {
    if let x = x as? SomeProtocol.SomeOtherProtocol {…}
}

- Dave Sweeris

···

On Apr 13, 2016, at 12:47 AM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

On Apr 8, 2016, at 8:53 AM, Shawn Erickson <shawnce@gmail.com <mailto:shawnce@gmail.com>> wrote:

I want to reiterate that I have objective-c code, others have objc code, and the cocoa, etc. frameworks have code that depend on optional protocol for things like (but not limited to) delegates. This is of course obvious but what seems to get lost in the discussion is that you can't always replace the non-existence of an implementation of an optional protocol method with a default implementation.

I have code that probes a delegate when registered and based on the what subset of the optional protocol methods it handles configures its runtime state to optimize itself to that reality. For example it may avoid allocating and maintaining potentially complex state if one or more methods are not implemented by the delegate (since no one is interested in it). If we just blindly provide default implementation for optional methods then this optimization couldn't take place.

I know others - including I believe Apple framework code - do similar optimizations based on what methods an object implements.

Just to be very clear (which I think my initial post wasn’t) my proposal does *not* break this optimization when a Swift class is conforming to an @objc protocol: even if a default is present, it won’t be visible to the Objective-C runtime at all.

My proposal *does* make it significantly harder to implement a check for “did the type implement this method?”, because one will effectively have to use -respondsToSelector:. For Cocoa-defined delegates, that doesn’t matter at all: apps generally implement requirements of delegates/data sources, but almost never go through the protocol to use those methods/properties. It’s the frameworks that do the calling, and of course they’re already using -respondsToSelector: checks.

The main effect is in Swift code that uses @objc optionals and tests for the absence of an implementation to perform some optimization. The tenor of the previous thread seems to indicate that this probably isn’t common, because there are probably better ways to model these cases in Swift—whether it’s with multiple protocols or something that specifically describes the policy (e.g., http://thread.gmane.org/gmane.comp.lang.swift.evolution/13347/focus=13480).

I think we should maintain the optional concept in support of bridging existing objc code into swift (confined to @objc)... unless a way to bridge things can be defined that avoids the loss of optimization potential I outlined above.

The direction I’m trying to go is not to have half of a feature—something that seems like it should be general, but is tied to @objc—in the language. We get a very large number of requests to make “optional” work for Swift protocols, because it’s a confusing limitation and there is a ton of overlap with default implementations. It would be far better to remove the feature.

Optional protocols don't need to be expanded into Swift itself since I believe alternate methods and patterns exists to solve the same type of need.

Given that you don’t feel that optional requirements need to work in Swift-only protocols, and what I’ve said above about compatibility with Cocoa, do you still think we need to keep ‘@objc optional’ as a notion in the language?

  - Doug

-Shawn

On Thu, Apr 7, 2016 at 5:12 PM Douglas Gregor via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <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
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


(Douglas Gregor) #16

Sent from my iPad

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.

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution. It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes, and your proposed solution makes this incredibly obscure and magical.

Do you have the same thought for optional closure properties? If so and heightForRow was an optional closure property it would satisfy all use cases elegantly. It could have a default implementation that returns nil. When non-uniform heights are required a normal method implementation can be provided. Delegates that have uniform row heights some of the time, but not all of the time, would also be supported by implementing the property.

There are still some issues here:

1) It doesn’t handle optional read/write properties at all, because the setter signature would be different. Perhaps some future lens design would make this possible. For now, the workaround would have to be importing the setter as a second optional closure property, I guess. (The current system is similarly broken).

2) For an @objc protocol, you won’t actually be able to fully implement the optional closure property with a property of optional type, because “return nil” in the getter is not the same as “-respondsToSelector: returns false”. Indeed, the getter result type/setter parameter type should be non-optional, so we would (at best) need a special rule that optional closure properties of @objc protocols can only be implemented by non-optional properties of closure type or by methods.

If we decide to favor this approach it would be really nice to be able to import Cocoa delegate protocols this way. Is that something that might be feasible?

Yes. If we favor this approach, it should be fairly direct to make imported Objective-C protocols work this way.

  - Doug

···

On Apr 11, 2016, at 10:30 AM, Matthew Johnson <matthew@anandabits.com> wrote:

On Apr 11, 2016, at 12:15 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:


(Joe Groff) #17

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.

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution.

I haven’t seen these come up in any discussion that wasn’t about mapping Objective-C optional requirements to something else in Swift. What other use cases are you envisioning?

The desire for labeled closure variables has come up a few times. For instance, it would provide a way to give local label names to closure arguments. In C, you can say:

  void doStuff(void (*onCompletion)(void *result, void *error)) {
    if (auto x = /*doStuff*/) {
      onCompletion(/*result*/ x, /*error*/ nullptr);
    } else {
      onCompletion(/*result*/ nullptr, /*error*/ getLastError());
    }
    onCompletion(/*result*/ x, /*error*/ y);
  }

where `result` and `error` don't affect the type, but describe the use of the parameters to the completion closure. It'd be nice to do the same in Swift:

  func doStuff(onCompletion completed(result:error:): (AnyObject?, AnyObject?) -> ()) {
    /*doStuff*/
    completed(result: x, error: nil)
  }

There's also the case of using if let to test for presence of a method, where you'd really like to keep the labels on the local binding:

  if let doStuff(to:with:) = object.doStuff(to:with:) {
    ...
  }

We currently lean on the vestige of labeled-tuples-as-function-arguments to do this, but I think we ultimately want to get away from that.

-Joe

···

On Apr 12, 2016, at 11:24 PM, Douglas Gregor <dgregor@apple.com> wrote:

On Apr 11, 2016, at 10:15 AM, Joe Groff <jgroff@apple.com> wrote:

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes,

Most of the requests I see for this feature are of the form “this works for @objc protocols, so it should work everywhere,” and most of the push-back I’ve seen against removing ‘optional’ is a concern over interaction with Cocoa. I haven’t gotten the sense that optional requirements are considered to be the best design for any particular task in Swift.

and your proposed solution makes this incredibly obscure and magical.

That’s fair. The mechanism I’m talking about *is* a bit hard to explain—we would need to rely on the diagnostic for cases where one tries to call a method that is caller-defaulted from Swift code, e.g.,

  error: method ‘foo(bar:wibble:)’ may not be implemented by the adopting class; add a default implementation via an extension to protocol ‘Foo'

This would only affect optional requirements of protocols imported from Objective-C. My hypothesis is that those just aren’t used in Swift app code.

  - Doug


(Dave Abrahams) #18

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.

···

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

(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@swift.org> wrote:

on Fri Apr 08 2016, Dietmar Planitzer <swift-evolution@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@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@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

--
Dave

_______________________________________________
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

--
Dave


#19

Hello,

I may have missed the point, but it looks like you say that optional delegate methods could be replaced by closures.

If this is the case, then I recently faced an issue with closures as a replacement for optional delegate methods, and the issue was with the responsibility of weak reference of the delegate.

So instead of:
  
  protocol CDelegate : class { func f() }
  class C {
    weak var delegate: CDelegate?
    func doIt() {
      delegate?.f()
    }
  }

We’d have:

  class C {
    var f: (() -> ())?
    func doIt() {
      f?()
    }
  }

Is it what you were referring to?

If so, then the trouble is for the code that sets the closure. It has to perform the weak self/strongSelf dance:

  c = C()
  c.f = { [weak self] in
    guard let strongSelf = self else { return }
    strongSelf….
  }

I find it awfully awfully heavy. The caller has 1. to remember about weakifying self, and 2. extract strongSelf from the weak self.

Of course, if you were talking about something else, you can discard my comment.

Gwendal Roué

···

On Apr 11, 2016, at 12:15 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway.


(Matthew Johnson) #20

Sent from my iPad

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.

To me, compound names for closure properties and satisfying property requirements with methods aren't hacks, they're missing features we ought to support anyway. I strongly prefer implementing those over your proposed solution. It sounds to me like a lot of people using optional protocol requirements *want* the locality of control flow visible in the caller, for optimization or other purposes, and your proposed solution makes this incredibly obscure and magical.

Do you have the same thought for optional closure properties? If so and heightForRow was an optional closure property it would satisfy all use cases elegantly. It could have a default implementation that returns nil. When non-uniform heights are required a normal method implementation can be provided. Delegates that have uniform row heights some of the time, but not all of the time, would also be supported by implementing the property.

There are still some issues here:

1) It doesn’t handle optional read/write properties at all, because the setter signature would be different. Perhaps some future lens design would make this possible. For now, the workaround would have to be importing the setter as a second optional closure property, I guess. (The current system is similarly broken).

I was only thinking about methods, not properties. :slight_smile: How common are optional, writeable property requirements? I don’t have a good guess off the top of my head, but my hunch is that they are not that common. If they aren’t, maybe a workaround would be acceptable (at least until a lens design comes along in the future).

2) For an @objc protocol, you won’t actually be able to fully implement the optional closure property with a property of optional type, because “return nil” in the getter is not the same as “-respondsToSelector: returns false”. Indeed, the getter result type/setter parameter type should be non-optional, so we would (at best) need a special rule that optional closure properties of @objc protocols can only be implemented by non-optional properties of closure type or by methods.

This is related to why I asked about feasibility. I know that “return nil” is not that same as a “respondsToSelector:” implementation that returns false if the property was implemented to return nil. Some magic would need to handle that translation to make it work with existing Objective-C protocols. This would automate what I have done in Objective-C several times by implementing respondsToSelector manually to hide protocol method implementations when necessary.

The advantage of going this route is that Swift implementations of the legacy Cocoa protocols will still function as expected in Cocoa while fitting the Swift model much better than optional protocol requirements.

···

On Apr 13, 2016, at 11:42 AM, Douglas Gregor <dgregor@apple.com> wrote:

On Apr 11, 2016, at 10:30 AM, Matthew Johnson <matthew@anandabits.com> wrote:

On Apr 11, 2016, at 12:15 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Apr 7, 2016, at 5:12 PM, Douglas Gregor via swift-evolution <swift-evolution@swift.org> wrote:

If we decide to favor this approach it would be really nice to be able to import Cocoa delegate protocols this way. Is that something that might be feasible?

Yes. If we favor this approach, it should be fairly direct to make imported Objective-C protocols work this way.

  - Doug