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

Reply via email to