[swift-evolution] [Idea] How to eliminate 'optional' protocol requirements
Dietmar Planitzer
dplanitzer at q.com
Tue Apr 12 16:32:59 CDT 2016
Inline.
> On Apr 11, 2016, at 10:03, Dave Abrahams <dabrahams at apple.com> wrote:
>
>
> on Sun Apr 10 2016, Dietmar Planitzer <dplanitzer-AT-q.com> wrote:
>
>>> On Apr 10, 2016, at 11:46, Dave Abrahams via swift-evolution
>> <swift-evolution at swift.org> wrote:
>>>
>>>
>>> on Sun Apr 10 2016, Dietmar Planitzer <swift-evolution at swift.org>
>> wrote:
>>>
>>>> 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.
>>
>> I know.
>>
>>>>
>>>> 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.
>>
>> You object why? I do understand why you object to the ObjC model since
>> there is not necessarily an implementation of the protocol method and
>> thus the protocol provider has to guard every call with an existence
>> check. But in this model here we would be guaranteed that there would
>> be an implementation of the protocol method and thus guarding the call
>> wouldn’t be necessary.
>
> Because it's a needless complication that will encourage protocol and
> algorithm designers to create inefficient programs because they know the
> user can fall back on this hack.
It’s not clear why you think that the ability to check whether a protocol adopter implements a method or doesn’t would make programs inefficient. The fact that the protocol provider can check whether an adopter actually wants a feature or not (optional method is implemented -> adopter wants the feature; otherwise he clearly doesn’t) is what enables us to create more efficient implementations.
> Nobody thinks that classes need the
> ability to check whether a given method is overridden. Why should this
> be needed for protocols?
Various Mac OS X frameworks have been using this technique for a long time to enable:
a) binary backward compatibility: eg NSDocument or NSBrowser in the AppKit have dramatically changed their respective implementations over the past 10 to 20 years and they have changed their subclassing APIs in some cases. The implementation does check whether an app is overriding the old methods and if so the classes enable backward compatibility functionality as needed while still providing as many of the new pieces of functionality as possible.
b) optimizations: eg the AppKit text system for example is very powerful, Unicode compliant and it offers a lot of customization hooks for app developers. However customization hooks are sometimes only enabled if they are actually used by an app (if the app overrides them in a subclass). Otherwise they are disabled and bypassed to enable faster standard text processing.
>>>> 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.
>>
>> It helps because it allows the protocol provider to *understand*
>> whether the protocol adopter is actually using a certain feature or
>> isn’t.
>
> If they need to understand that, they can make the indicator of that
> fact a separate protocol requirement.
>
>> Here is the table view example again:
>>
>> func useDelegate(delegate: NSTableViewDelegate) {
>>
>> if has_override(delegate, tableView(_:, heightForRow:)) {
>> // call tableViewNumberOfRows() on the delegate
>> // allocate the geometry cache (1 entry per row)
>> // call tableView(_:, heightForRow:) for each row
>> } else {
>> // nothing to do here since here all rows have the same height
>> }
>> }
>>
>> Note that has_override() is just a placeholder syntax because I’ve not
>> had a good idea yet of how to express this in a Swiftier way.
>
> if delegate.hasVariableSizedRows { … }
This means that we not only increase the surface of the API, it also means that we make it now possible for the protocol adopter to provide conflicting information to the caller of the protocol methods. At the same time, the caller of the protocol methods still needs to guard every call of tableView(heightForRow:) with a call to hasVariableSizedRows(). So by the end of the day, this solution doesn’t improve anything. It just adds new complexity and sources of bugs.
The surface of the API increases because the protocol adopter now has to learn 2 methods instead of 1 and he has to provide up to 2 implementations instead of just one. It makes it more likely that the protocol adopter writes buggy software and gets confused by the behavior of the table view. Eg it is safe to assume that both hasVariableSizedRows() and heightForRow() would come with a default implementation (keep in mind that those features didn’t even exist for the first couple years of the NSTableView’s life). So I could write this in my delegate:
func heightForRow(row: Int) -> Float {
return (i % 2) == 0 ? 60 : 30
}
and then when I compile my app and run it - I don’t get alternate row heights because if forgot to provide my own implementation of hasVariableSizedRows() (and the default implementation returns false for binary backward combat reasons). Or I write this:
func hasVariableSizedRows() -> Boolean {
return true
}
func heigthForRow(row: Int) -> Float {
return (i % 2) == 0 ? 60 : 30
}
Compile it - and again the table view doesn’t work “thanks” to the fact that the protocol comes with default implementations AND because Swift does not require that a method which overrides a default protocol implementation must be marked with the “override” (or some other) keyword. Eg this:
override func hasVariableSizedRows() -> Boolean {
return true
}
override func heigthForRow(row: Int) -> Float {
return (i % 2) == 0 ? 60 : 30
}
Here at least the compiler would have been able to tell me that something is wrong and that I’m not actually replacing the default protocol method implementation as I thought I would. Yes the ObjC variant of optional protocol methods also suffers from the misspelling problem - but that’s not an argument against the concept of optional protocol methods. It is only an argument against the way that they are implemented in ObjC. One of my hopes, when Swift was revealed to the public as “an Objc without the C part”, was actually that this would have been one of the things that it would have fixed. But it didn’t. Instead it gave us a similar problem with its particular combination of default protocol methods and the way they are supposed to be overriden / shadowed in conforming types.
So, no. Your proposed solution is not an improvement. Especially considering the way that protocols work in Swift today.
Your model also allows a protocol adopter to provide conflicting information because he can now do this:
func hasVariableSizedRows() -> Boolean {
return false
}
func heightForRow(row: Int) -> Float {
return 8
}
or he can do this:
func hasVariableSizedRows() -> Boolean {
return true
}
func heightForRow(row: Int) -> Float {
return 0
}
and then the question is: what should the table view do? Does the return value of hasVariableSizeRows() represent the truth or is it the return value of heightForRow()? It’s possible to argue either way. But no matter how we decide to handle this, we now need to write code in the protocol using type that detects these conflicts and does something about them. Even if that something is just to kill the app, we still need to write that code correctly. If we miss a case and it just so happens that an app that returns conflicting information to us gets away with it and the table view ends up producing something that looks right to the app vendor, then we are now forever bound to keep this special case intact since breaking an app with a OS update is not an option.
Finally, this model didn’t actually make anything safer from the viewpoint of the protocol provider because we still need to guard every call to heightForRow(). Nothing has improved on that front:
if delegate.hasVariableSizeRows() {
return delegate.heightForRow(row)
} else {
return rowHeight
}
is not better than this:
if has_override(delegate, heightForRow) {
return delegate.heightForRow()
} else {
return rowHeight
}
in fact it is worse. Because we now need to introduce this “guarding predicate” for every single use case. Each and every use case will require yet another guarding call that we have to design, implement, test and that our users have to learn about. It is much better to solve this problem once and then to reuse this solution for all use cases since it is universally applicable.
What this is fundamentally about, and allows us to do:
if delegate.respondsToSelector(#selector(tableView(:heightForRow:)) {
delegate.tableView(self, heightForRow: row)
}
in ObjC is the idea that the NSTableView is a component which offers a set of features:
- fixed row heights
- variable row heights
- drag & drop
- old style pasteboard support
- new style pasteboard support
- old style table cells
- new style table cell views
- etc, pp
and then the delegate simply picks the features that it wants to use. It simply does this by implementing the corresponding methods and by NOT implementing the methods of a feature it doesn’t care about:
- delegate wants feature X -> implement the method for feature X
- delegate does not want feature X -> nothing to do
and this is why the concept of optional protocol methods captures this idea precisely: I don’t need to write code for feature X if I don’t want it. Why should I write code for something I don’t want? Wouldn’t make sense.
> if !(delegate is NSUniformTableViewDelegate) { … }
I don’t think that replacing 1 protocol with a dozen or more protocols is a good idea or a step forward. Beside that we would actually have to introduce hundreds of new protocols since the optional protocol method feature is used all over the place in public Mac OS X frameworks and also in private ones.
> etc.
>>
>> In this example the table view is able to check whether the protocol
>> adopter has actually “overriden” the default implementation of
>> tableView(_:, heightForRow:).
>
> Which, IMO, is a terrible way to indicate that a view has variable row
> heights. It's indirect and maybe even inaccurate (I can imagine a table
> view that is uniform and has its height set up once at construction
> time, therefore it needs to override heightForRow).
I don’t see how it is indirect. I want the feature -> I implement the corresponding method; I don’t want the feature -> I don’t write code for it. This captures exactly the nature of the problem and it really can’t get any simpler than that for the protocol user. Yes, it makes life a bit harder for the protocol provider. But that is absolutely fine and in fact that is what we want since for every protocol provider, there are dozens to hundreds to thousands of developers who are going to write code which adopts the protocol. So naturally we want to focus the complexity of the implementation on the side of the protocol provider while making the life for protocol adopters as easy and safe as possible.
It’s not clear how the optional protocol method concept can cause inaccuracy except that a protocol provider decides to misuse it. If I want a table view with a fixed row height then I just set the row height on the table view like this:
tableView.rowHeight = 16
and I simply don’t provide a heightForRow() implementation since all I want is a single height for all rows. So parameterizing the row height is not necessary and writing code for that would be a waste of time and just be an unnecessary source of bugs and potential confusion.
Regards,
Dietmar Planitzer
>> If the adopter did, then the table view knows that the adopter wants
>> variable row heights and thus the table view can now create a cache of
>> row heights and it can enable the layouting code that knows how to lay
>> out rows with different heights. If however the adopter did not
>> provide its own implementation of this method then the table view does
>> not need to create a geometry cache and it can switch over to the
>> simpler fixed-row-height layout code. The reason why we want to cache
>> the row heights in the table view is because computing those heights
>> can be nontrivial and the layout code needs to access those height
>> values in every layoutSubviews() call. And layoutSubviews() is invoked
>> 60 times per second while the user is scrolling. Also keep in mind
>> that, if we would not cache the row heights, then the row height
>> computation would end up competing for CPU cycles with the code that
>> properly configures the views for each row.
>>
>> Without the ability to do this check on the protocol provider side, we
>> are forced to increase the API surface so that the protocol adopter
>> can explicitly tell us which layouting model he wants.
>
> That's exactly what one should do. If layout model is an important
> feature, the adopter should be explicit abou tit.
>
>> But this also means that the protocol adopter now has to remember that
>> he needs to configure the layouting option correctly in order to get a
>> working and efficiently working table view. So the end result would be
>> a table view that’s hard to use correctly.
>>
>> Regards,
>>
>> Dietmar Planitzer
>>
>>>>
>>>> (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 at swift.org> wrote:
>>>>>
>>>>>
>>>>> on Fri Apr 08 2016, Dietmar Planitzer <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.
>>>>>
>>>>> 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 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
>>>>>
>>>>> --
>>>>> Dave
>>>>>
>>>>> _______________________________________________
>>>>> 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
>>>
>>> --
>>> Dave
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>
> --
> Dave
More information about the swift-evolution
mailing list