>
> protocol A {
> func f()
> func x()
> }
>
> extension A {
> func x() {print("a-x")}
> }
>
> class B: A { // already strange. B depends on A extension. did not implement
> all required methods from A
> func f() {}
> }
In order to understand the various perspectives on what constitutes expected
versus strange, it might be useful to have a sense of which programing language
the viewpoint would be expressed from.
For eg in this case, coming from objc it might indeed be surprising that one of
the methods of Protocol A does not have to be implemented (this was well
explained in last year's wwdc, or was it even 2 years ago). Coming from a java
viewpoint however, this would present no surprise, except for having to write
the default implementation in an extension rather than directly in the protocol
itself. Scala, c#... and more? again different kinds of surprises, but overall
the pleasant feeling that swift is actually a modern language.
I took the liberty to rewrite the examples with different variable name to
avoid mixing expectations with behavior:
// ——————————————
protocol P {
func x()
}
extension P {
func x() {
helper()
print("ext-x")
}
func helper() {
print("ext-helper")
}
}
class A:P {
func x() { // Note that ‘override’ is not required, even though in
effect
print("a-x”) // the local x() implementation is an override of the
default
} // supplied by the protocol extension. However at the
same time,
} // by virtue of being defined inside an extension of
the protocol
// it is reasonable to consider that the default
implementation
// is NOT intrinsically a part of the protocol,
defining an
// implementation of x() inside a conforming class is
NOT
// considered overriding the definition existing in the
extension
class B:P { // B is made to reliant on the extension for its A
conformance
func helper() {
print("b-helper")
}
}
class C:B {
func x() { // Note that ‘override’ is not required because B does
not provide
print ("c-x”) // its own implementation of x() (wasn’t there a
proposal from
} // E.Sadun regarding ‘override’ at this location?!)
override func helper() { // Here ‘override’ is mandated by the presence
of a similar
print("c-helper”) // helper inside B
}
}
// ——————————————
// invocation via the object type
var x1 = A()
x1.x() // a-x no surprise
x1.helper() // ext-helper no surprise
var x2 = B()
x2.x() // ext-helper + ext-x !!! no surprise even if 'b-helper +
ext-x’ might seem more ‘intuitive'
x2.helper() // b-helper no surprise
var x3 = C()
x3.x() // c-x no surprise
x3.helper() // c-helper no surprise
The direct invocation case is mostly without surprises, and in all cases,
logically explainable. The only contentious point might be why the definition
of helper() present in B is not used when helper() is invoked from the default
method implementation supplied in the protocol extension.
// ——————————————
// invocation via the protocol type
var v1:P = A()
v1.x() // a-x no surprise (type has precedence over
default when directly equivalent)
v1.helper() // ext-helper no surprise
var v2:P = B()
v2.x() // ext-helper + ext-x coherent with x2.yyy() calls
v2.helper() // ext-helper entirely coherent, even if possibly
surprising
var v3:P = C()
v3.x() // ext-helper + ext-x !!! again this is surprising on the
surface, but it stems from the lack
v3.helper() // ext-helper of direct link to P. So when it comes
to dealing with C as a
reference to a P, there is no
alternative but to refer to B to
find out what to do
So we have identified some cases where depending on which programming language
we might come from, there might be a mismatch between expectations and current
Swift behavior, leading to possible bugs and or frustrations. Considering that
nothing says that one line of intuition is more right than any other or even
than the existing behavior, it may still be useful to manage expectations
differently than they are today.
If the desire is to align the current code with the one line of
expectations/intuition mentioned above, then it seems that the alternatives are
the following:
1) Allow ‘override’ at the point of definition of x() inside C() (despite the
absence of a x() definition inside B). The same could be said of the definition
of helper() inside B.
One issue with this scenario is that technically speaking, the definition of
helper() inside B or x() inside C are NOT overrides, because the methods they
define are NOT a part of the protocol. This stems directly from the fact that
default protocol methods in extensions are an extension of the internal
resolution mechanism that is NOT a part of the formal definition of the
protocol they supplement (see #5 for a solution that would make them FORMALLY a
part of the protocol itself). IMO this semantic gap should eliminate this
solution entirely
2) Support the following calling convention
straw_man_dynamic_dispatch v2.x() // ext-helper + ext-x (NOTE: does
leave an expectation mismatch regarding 'b-helper’)
straw_man_dynamic_dispatch v2.helper() // b-helper
straw_man_dynamic_dispatch v3.x() // c-x
straw_man_dynamic_dispatch v3.helper() // c-helper
In this scenario, the user of P would express the desire to include any object
type level redefinitions take precedence over any possible default behavior she
might have provided in a protocol extension. Note that it does leave a possible
expectations mismatch regarding the call to helper() from within the context of
a dynamically resolved parent call. This could also be resolved by deciding
that once-dynamic, always dynamic which would create more cognitive overload by
having to trace every call-tree...
3) Extend 2) to all call sites of x() by making the annotation on the method
inside protocol extension
extension P {
straw_man_dynamic_dispatch func x() {
helper()
print(“ext-x”)
}
fun helper() {
print(“ext-helper”)
}
}
v1.x() // a-x
v1.helper() // ext-helper
v2.x() // ext-helper + ext-x might surprise some, but once again
logical as helper() is NOT straw_man_dynamic_dispatch
v2.helper() // ext-helper
v3.x() // c-x
v3.helper() // ext-helper again complete logical as helper() in P
is NOT straw_man_dynamic_dispatch and C has no formal relationship to P
4) change the default behavior for dispatching calls to default methods in
protocol extensions, and provide an annotation that indicates to opposite
behavior per call-site and/or for all call-sites
extension P {
straw_man_static_dispatch func x() {
print(“ext-x”) STATICALLY dispatched
}
fun helper() {
print(“ext-helper”) dynamic dispatch
}
}
v1.x() // a-x
v1.helper() // ext-helper
v2.x() // b-helper + b-x
v2.helper() // b-helper
v3.x() // ext-x
v3.helper() // c-helper
5) leave things the way there are today, and support dynamically dispatched
protocol defaults via a new default methods mechanism on protocol directly
protocol P {
straw_man_default_attribute func x() {
print(“proto-x”)
}
}
v1.x() // a-x
v1.helper() // ext-helper
v2.x() // proto-x
v2.helper() // ext-helper
v3.x() // c-x - note that ‘override’ would then be REQUIRED
inside the implementation of C().
v3.helper() // ext-helper again local due to he absence of direct
relationship between C and P (it is all via B-ness)
Regardless of the path chosen, there seems to be room today from more
information from the compiler.
@michael
Can we agree that two methods with the same name sometimes have the same
contract and sometimes not? And that this is not a programmer error? And that
it would be good to distinguish between these two cases?
yes on all accounts.
NOTES:
a reasonable candidate for the straw_man_dynamic_dispatch attribute may very
well be the existing dynamic
a reasonable candidate for the straw_man_default_attribute attribute might be
default
a reasonable candidate for the straw_man_static_dispatch attribute might be:
nondynamic
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution