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

Shawn Erickson shawnce at gmail.com
Sat Apr 9 09:22:21 CDT 2016


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

> The biggest missing part with this model is that we are still not able to
> enable macro-level optimizations in the delegating type by checking whether
> the delegate does provide his own implementation of an optional method or
> doesn’t. However, this is an important advantage of the ObjC model that we
> should not lose.
>
> Maybe it’s time to take a big step back and ignore the question of how to
> implement things for a moment and to instead focus on the question of what
> the conceptual differences are between ObjC protocols with optional methods
> and Swift protocols with default implementations. There are two relevant
> viewpoints here:
>
> 1) From the viewpoint of a protocol adaptor:
>
> ObjC:
>
> 1a) adopter may provide his own implementation of the protocol method, but
> he is no required to.
>
> 1b) adopter can see in the protocol declaration for which methods he must
> provide an implementation. Those methods do not have the “optional” keyword
> in front of them while optional methods do.
>
> Swift:
>
> 1c) same as (1a).
>
> 1d) opening a binary-only Swift file in Xcode with a protocol definition
> in it which contains methods with default implementations will not give any
> indication of which method has a default implementation and which doesn’t.
> It’s only possible to see a difference on the syntax level if you have
> access to the sources.
>
> 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 at swift.org> wrote:
> >
> > Hi all,
> >
> > Optional protocol requirements in Swift have the restriction that they
> only work in @objc protocols, a topic that’s come up a number of times. The
> start of these threads imply that optional requirements should be available
> for all protocols in Swift. While this direction is implementable, each
> time this is discussed there is significant feedback that optional
> requirements are not a feature we want in Swift. They overlap almost
> completely with default implementations of protocol requirements, which is
> a more general feature, and people seem to feel that designs based around
> default implementations and refactoring of protocol hierarchies are overall
> better.
> >
> > The main concern with removing optional requirements from Swift is their
> impact on Cocoa: Objective-C protocols, especially for delegates and data
> sources, make heavy use of optional requirements. Moreover, there are no
> default implementations for any of these optional requirements: each caller
> effectively checks for the presence of the method explicitly, and
> implements its own logic if the method isn’t there.
> >
> > A Non-Workable Solution: Import as optional property requirements
> > One suggestion that’s come up to map an optional requirement to a
> property with optional type, were “nil” indicates that the requirement was
> not satisfied. For example,
> >
> > @protocol NSTableViewDelegate
> > @optional
> > - (nullable NSView *)tableView:(NSTableView *)tableView
> viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row;
> > - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row;
> > @end
> >
> > currently comes in as
> >
> > @objc protocol NSTableViewDelegate {
> >   optional func tableView(_: NSTableView, viewFor: NSTableColumn, row:
> Int) -> NSView?
> >   optional func tableView(_: NSTableView, heightOfRow: Int) -> CGFloat
> > }
> >
> > would come in as:
> >
> > @objc protocol NSTableViewDelegate {
> >   var tableView: ((NSTableView, viewFor: NSTableColumn, row: Int) ->
> NSView?)? { get }
> >   var tableView: ((NSTableView, heightOfRow: Int) -> CGFloat)? { get }
> > }
> >
> > with a default implementation of “nil” for each. However, this isn’t
> practical for a number of reasons:
> >
> > a) We would end up overloading the property name “tableView” a couple
> dozen times, which doesn’t actually work.
> >
> > b) You can no longer refer to the member with a compound name, e.g.,
> “delegate.tableView(_:viewFor:row:)” no longer works, because the name of
> the property is “tableView”.
> >
> > c) Implementers of the protocol now need to provide a read-only property
> that returns a closure. So instead of
> >
> > class MyDelegate : NSTableViewDelegate {
> >   func tableView(_: NSTableView, viewFor: NSTableColumn, row: Int) ->
> NSView? { … }
> > }
> >
> > one would have to write something like
> >
> > class MyDelegate : NSTableViewDelegate {
> >   var tableView: ((NSTableView, viewFor: NSTableColumn, row: Int) ->
> NSView?)? = {
> >     … except you can’t refer to self in here unless you make it lazy ...
> >   }
> > }
> >
> > d) We’ve seriously considered eliminating argument labels on function
> types, because they’re a complexity in the type system that doesn’t serve
> much of a purpose.
> >
> > One could perhaps work around (a), (b), and (d) by allowing compound
> (function-like) names like tableView(_:viewFor:row:) for properties, and
> work around (c) by allowing a method to satisfy the requirement for a
> read-only property, but at this point you’ve invented more language hacks
> than the existing @objc-only optional requirements. So, I don’t think there
> is a solution here.
> >
> > Proposed Solution: Caller-side default implementations
> >
> > Default implementations and optional requirements differ most on the
> caller side. For example, let’s use NSTableView delegate as it’s imported
> today:
> >
> > func useDelegate(delegate: NSTableViewDelegate) {
> >   if let getView = delegate.tableView(_:viewFor:row:) { // since the
> requirement is optional, a reference to the method produces a value of
> optional function type
> >     // I can call getView here
> >   }
> >
> >   if let getHeight = delegate.tableView(_:heightOfRow:) {
> >     // I can call getHeight here
> >   }
> > }
> >
> > With my proposal, we’d have some compiler-synthesized attribute (let’s
> call it @__caller_default_implementation) that gets places on Objective-C
> optional requirements when they get imported, e.g.,
> >
> > @objc protocol NSTableViewDelegate {
> >   @__caller_default_implementation func tableView(_: NSTableView,
> viewFor: NSTableColumn, row: Int) -> NSView?
> >   @__caller_default_implementation func tableView(_: NSTableView,
> heightOfRow: Int) -> CGFloat
> > }
> >
> > And “optional” disappears from the language. Now, there’s no optionality
> left, so our useDelegate example tries to just do correct calls:
> >
> > func useDelegate(delegate: NSTableViewDelegate) -> NSView? {
> >   let view = delegate.tableView(tableView, viewFor: column, row: row)
> >   let height = delegate.tableView(tableView, heightOfRow: row)
> > }
> >
> > Of course, the code above will fail if the actual delegate doesn’t
> implement both methods. We need some kind of default implementation to fall
> back on in that case. I propose that the code above produce a compiler
> error on both lines *unless* there is a “default implementation” visible.
> So, to make the code above compile without error, one would have to add:
> >
> > extension NSTableViewDelegate {
> >   @nonobjc func tableView(_: NSTableView, viewFor: NSTableColumn, row:
> Int) -> NSView? { return nil }
> >
> >   @nonobjc func tableView(_: NSTableView, heightOfRow: Int) -> CGFloat {
> return 17 }
> > }
> >
> > Now, the useDelegate example compiles. If the actual delegate implements
> the optional requirement, we’ll use that implementation. Otherwise, the
> caller will use the default (Swift-only) implementation it sees. From an
> implementation standpoint, the compiler would effectively produce the
> following for the first of these calls:
> >
> > if delegate.responds(to:
> #selector(NSTableViewDelegate.tableView(_:viewFor:row:))) {
> >   // call the @objc instance method with the selector
> tableView:viewForTableColumn:row:
> > } else {
> >   // call the Swift-only implementation of tableView(_:viewFor:row:) in
> the protocol extension above
> > }
> >
> > There are a number of reasons why I like this approach:
> >
> > 1) It eliminates the notion of ‘optional’ requirements from the
> language. For classes that are adopting the NSTableViewDelegate protocol,
> it is as if these requirements had default implementations.
> >
> > 2) Only the callers to these requirements have to deal with the lack of
> default implementations. This was already the case for optional
> requirements, so it’s not an extra burden in principle, and it’s generally
> going to be easier to write one defaulted implementation than deal with it
> in several different places. Additionally, most of these callers are
> probably in the Cocoa frameworks, not application code, so the overall
> impact should be small.
> >
> > Thoughts?
> >
> >       - Doug
> >
> > _______________________________________________
> > swift-evolution mailing list
> > swift-evolution at swift.org
> > https://lists.swift.org/mailman/listinfo/swift-evolution
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160409/5a6876ff/attachment.html>


More information about the swift-evolution mailing list