Default Implementation in Protocols

Yes!! Thanks, @Alejandro :-)

There are plenty of things the compiler doesn't (/shouldn't/can't) enforce. It doesn't prevent you writing 1,000-line functions, but you shouldn't. It doesn't prevent you open-coding for loops that replicate higher-order std lib functions like map, but you shouldn't. And it doesn't stop you declaring all your methods and conformances inside the initial struct definition. But you shouldn't.

While my disagreement with this pitch is just my personal opinion, the way the standard library organizes code is more than a personal preference. It is explicitly a style that the core team encourages, as stated in the acceptance rationale of SE-0169.

4 Likes

I think these show up in the documentation today.

1 Like

(I'll play the devil's advocate)

You shouldn't, but sometimes you just have to. As the linked rationale says:

The core team expects future language evolution to reinforce the notion that extensions are more of a code organization tool than distinct entities, e.g., allowing stored properties to be introduced in an extension.

In my own practice, I'm not totally consistent. I usually split my types with extensions, but it also happens that I merge them all. Especially when the type is tiny and is better grasped as a whole.

Besides, extensions are not only a way to group related apis. They are also a way to workaround language gotchas. For example, when you want to keep the generated memberwise struct initializer, you have to add other initializers in an extension. Even when you don't want to do it.

To me, the linked rationale is overly optimistic and simplistic. It doesn't describe the full reality of extensions.

1 Like

Ah, yeah, I wasn't counting the documentation. (Though I do think there's room for more nuance there—under what conditions is there a default implementation?)

I wonder whether there's some synergy with the discussion around conditional conformances on opaque result types. If we go with one of the proposed syntaxes for in-line conditional conformances there, maybe we could also use it to denote conditional default implementations in protocols?

3 Likes

That covers cases where it's the same implementation, but not where it's a different one.

It's a very intuitive and concise solution for a real problem - but to cite someone more important than me on the syntax in question:

As for the points against the pitch:
The argument of more compact declarations ultimately leads us to re-add header files, and I wouldn't want that to happen.
Although splitting things into extensions is very common, this is just because some influencers like that style, and not because there's any evidence that this might be beneficial. I'd even say it's actively harmful, with SE-0169 as an example for fallout caused by same-file extensions.

Last but not least, no one would be forced to use the new syntax - and I don't think there's any danger of causing confusion.

2 Likes

and here i thought one of Swift’s key selling points was getting rid of that god awful C++ declaration–definition style where every function signature gets copy and pasted twice

4 Likes

This presupposes that every function is both a customization point and defaulted, without constraint.

2 Likes

I think the standard library is a bit of an extreme case in how far it applies conditional conformances and multiple overlapping default implementation candidates. I'd bet that unconditional default implementations are by far the common case elsewhere.

5 Likes

Yes, I'd prefer Xcode (or third party IDEs) deal with that end of things.

If it's not easy enough to view barebones protocol interface, or C style headers, that's should be on the IDE. For example, there's no reason Xcode couldn't support a feature where "View -> Generated Headers as files" makes a bunch of generated h files pop up in the Files pane.

Having certain protocol features one must wrap in an "extension" isn't a big deal for large Protocol. It's distracting though, for tiny protocols. Like, "I could have put these three little procotols in one file, and they'd be perfectly readable, but now I have three files and six blocks of code"

3 Likes

I prefer not being forced to create an extra extension and always have stuff fragmented. I want to be able to keep things together when it helps and i want to be able to split things up when it helps. Beyond that it is something that should be part of linting and team/project conventions.

4 Likes

I echo @phlebotinum thoughts and would add that it is incongruous that class and structs can have inline definitions but not protocols.

3 Likes

While this could seem more approachable at first, I think it would end up being more confusing. We will always need to be able to provide default implementations inside of extensions because that is the only way to add conditional constraints. Being able to do this nice short-hand version only for default implementations without constraints makes the system seem more fragmented.

Take the following example:

protocol Foo {
    var readable: String { get }

    func bar() {
        // some default impl here
    }
}

extension Foo where Self: CustomStringConvertible {
    var readable: String { return description }
}

As a newcomer to Swift, I would probably be very confused as to why the syntax for these two default implementations is so different. I think it would make it harder to understand that they are really doing the same thing.

4 Likes

But people don't find:

struct S {
    func foo() {}
}
extension S: CustomStringConvertible {
    var description: String {
        return "S"
    }
}

Confusing, so why would they find the protocol version confusing. Someone above also pointed out that where clauses on extensions aren't that common on non-library code.

Swift is somewhat unique in even allowing many conditionally-chosen default implementations by overload resolution the way we do. Other languages with trait-like features generally make you define a new refining trait with additional constraints, and you put the default implementation on that trait and have conformers explicitly implement that trait to use it. That technique would work with Swift as well.

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: https://gist.github.com/erica/f749ae15e13ecde5e2762fe91a0ea149. 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) 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) 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

I see this from a different perspective. It seems more fragmented to me to be able to have implementations in the declarations of concrete types (structs and classes) and not in protocols. Most of the objections raised in this thread apply equally to concrete types (e.g. implementations can make it hard to skim for the interface, conditional conformances can only be written in extensions), and should probably be handled at the linter/style guide level. I don't recall anyone proposing that methods on concrete types only be allowed in extensions, though that would be another way to resolve the difference.

I'm actually very surprised by the negative response in this thread, because whenever this had been mentioned previously on the Swift discussion forums/mailing list it seemed to be agreed that allowing this was inevitable (but low priority), just waiting on the design and implementation work. Personally I'm in favour of anything that helps remove these inessential and confusing differences between protocols and concrete types, including this pitch and future work to make protocols with associated types more ergonomic.

5 Likes

I agree with Ben on this. In my opinion, having default implementation (conditional or unconditional) in an extension promotes consistency.

I just don't think of protocols the same way I think of classes or structs.

1 Like