Default Implementation in Protocols

Thank you all for initial feedback on this pitch! I want to lay out some of the different arguments against this along with providing some of my solutions to these problems. Each subject on its own is probably worth its own thread/proposal (not sure fitting everything in one proposal is a great idea). How each subject relates to default implementations in protocols I will get to at the end of each subject. There's a lot of information to unravel:

  1. Distinguishing between a protocol requirement's default implementation and a regular extension method:

Without having implemented default implementation in protocols, this is still considered a standing problem by some. @Erica_Sadun wrote a proposal on this subject here: roles2.md ยท GitHub. There is a lot of existing feedback in this topic along with other "bugs" that this proposal brings to light. I won't get too in detail here, but her proposal introduces a set of new keywords to help mitigate this problem. @Tino 's solution (IMO a very elegant solution) to the problem (here: Introducing role keywords to reduce hard-to-find bugs - #33 by Tino) is to require the requirement's implementation name to be prefixed by the protocol's name followed by a . or ::.

protocol Foo {
  // Normal protocol requirement with no default implementations
  func bar()
}

extension Foo {
  // Default implementation of bar in Foo
  func Foo.bar() {}
  // or
  func Foo::bar() {}
}

There are many pros as to why we would want to solve this problem such as compile time diagnostics (renaming bar to barr: "bar" is not a requirement of "Foo").
How does this relate to default implementations in protocols? Consider the example:

protocol Foo {
  // Normal protocol requirement with no default implementations
  func bar()
}

protocol Foo2: Foo {
  // Default implementation of bar in Foo
  func bar() {
    print(16)
  }
}

It's not initially clear that bar() comes from Foo. One can make the assumption that Foo2 actually declares its own requirement of bar() that comes with a provided default implementation. Such a solution would mitigate this by requiring us to write something like the following:

protocol Foo {
  // Normal protocol requirement with no default implementations
  func bar()
}

protocol Foo2: Foo {
  // Default implementation of Foo's bar
  func Foo.bar() {
    print(16)
  }
}

It's now clear that this implementation is implementing Foo's bar().

Also, one can make the argument that moving the unconstrained default implementation into the protocol can somewhat mitigate this. However for constrained extension implementations this is still a problem. Which leads me to my next point...

  1. We still need to write default implementations in constrained extensions

While I cannot disagree with this, I think it might be interesting an question to ask whether or not we can inline extension constraints. For example:

protocol A {}

protocol Foo {
  // Normal protocol requirement with no default implementations
  func bar()

  // Default implementation of bar when we conform to A
  func bar() where Self: A {
    print(16)
  }
}

// Which would be synonymous with
extension Foo where Self: A {
  func bar() {
    print(16)
  }
}

We can use this with the previous solution to be able to write something like:

protocol A {
  // Normal protocol requirement with no default implementations
  func bar()
}

protocol B {}

protocol Foo: A {
  // Default implementation of A's bar when we conform to B
  func A.bar() where Self: B {
    print(16)
  }
}

This works well with simple extension constraints, but doesn't tackle the problem about inlining conditional conformance. Forgive me if I interpreted this incorrectly, but @Joe_Groff suggested that some discussions between this proposal and opaque result types might have some synergy due to the nature that we can't write inline conditional conformance. While I do not have a current solution to such problem, I think if we tackle inlining conditional conformance, the absolute need for extensions to implement default implementations of requirements goes away (IIRC).

After having initial feedback and writing this up, my priorities now are to see if we can tackle these problems first before implementing default implementations in protocols. I still believe default implementations in protocols are not only a DRY win, but an expressive win. Currently we can write requirements coming from conforming types in the same decl, aswell as breaking it up into extensions. We can only write extensions where we want type constraints and/or conditional conformance. I do not believe that the language should force a certain code style onto the user, but rather I think the language should provide enough leeway to the user to at least be able to write everything in a struct/class/enum and even a protocol. At that point its up to the team/linter/personal style to decide what code style it respectively prefers. I share sentiment with Doug (Introducing role keywords to reduce hard-to-find bugs - #39 by Douglas_Gregor) that being able to write unconstrained default implementations in protocols feels more natural to the user. I go a step further in saying that maybe we should look at extensions as an optional code style.

3 Likes