Default Implementation in Protocols

Hello Evolution,

I've been working on implementing Default Implementation in Protocols and I have a working early implementation of the feature.

Current Implementation: [WIP] Default implementation in protocols by Azoy · Pull Request #19116 · apple/swift · GitHub

Refer to: swift/GenericsManifesto.md at main · apple/swift · GitHub .

To recap what this feature adds, today you can declare default implementation of a protocol requirement by extending the protocol:

protocol Animal {
  func makeNoise()
}

extension Animal {
  func makeNoise() {
    print("Bark!")
  }
}

struct Dog: Animal {}

let sparky = Dog()
sparky.makeNoise() // Bark!

What this feature allows is that you can now declare a default implementation of a requirement from the protocol:

protocol Animal {
  func makeNoise() {
    print("Bark!")
  }
}

struct Dog: Animal {}

let sparky = Dog()
sparky.makeNoise() // Bark!

I will hopefully be able to write up a formal proposal soon, but would love feedback from the community before I move further.

26 Likes

Great to see some progress. This is an often requested feature.

So, this adds no new functionality? For the purpose of distinguishing an actual extension member (that looks similar) from a default implementation, an annotation on the definition would serve the same need, right?

This will be a nice improvement. I look forward to deleting a couple hundred lines of boilerplate from my current project :)

I also think this will be much more accessible to beginners and people coming from other programming languages that do similar things.

1 Like

We’re not changing anything with how these kinds of default implementations are dispatched correct? IMO if we could start from scratch this is how it should have been. It makes it somewhat more obvious that this is a default and not some statically dispatched extension method.

2 Likes

Correct.

2 Likes

Personally, I am against adding it to the language.

We've worked hard in the standard library codebase to move towards code that keeps struct declarations as short and simple as possible, moving implementations into extensions with logical groupings – usually protocol conformances. Yes, this takes a little extra code to break out the extensions, but the result is much clearer. Being able to structure code like this was a major motivation in the changes to private visibility.

The reason for this is to keep code you need to keep in your head to understand it to a minimum, and not have to go hunting for answers scattered throughout a struct declaration. The key question (for an implementor – users should be looking at generated interfaces) when looking at a struct is "what is this thing? what are its fundamental stored properties? What's the basic (maybe internal) way to create one?". Then you look at its protocol conformances and ask "how does this type conform to this protocol?". In each case, you are looking at hopefully less than a screenful of text. Sometimes that's a stretch but it's the goal.

I feel the same about protocols. At a glance, I should be able to look at a protocol and ask "what is the fundamental nature of this protocol?". I shouldn't have to scroll, or store too information in my head, to grok what the fundamentals of the protocol are: its inherited protocols, some associated types, and the basic customization points. That is it. Stuffing the implementation code in there as well would be actively harmful to this.

It also weakens the important distinction between customization points and regular extensions. There is a key difference between a method that appears in a protocol, and a method that is just an extension. When teaching a new Swift programmer about this distinction, the fact that the method is declared in two places seems like a feature, not a bug. For most useful protocols, which have non-customization point extensions or constrained default implementations, you will still need to write extensions. Allowing methods that happen to be customization points to be defined inside the body of the protocol seems odd. I fear adding this feature will degenerate into too many methods being defined in protocols, whether they need to be customization points or not.

It's unclear what the motivation is here. DRY, I guess, in that you don't need to restate the method signatures. That's a typing reduction, but hopefully one that becomes less important over time with better tooling. (in fairness, you could make the tooling argument as a reason it's OK to put bodies in the declaration, because folding). Typos in default implementations are a risk, but one this only partly mitigates rather than eliminates — I'd prefer solutions like requiring an implements keyword or generating near-miss warnings.

38 Likes

Personally I share this feeling as well. And I also agree that making it easier to avoid accidentally declaring an extension method would alleviate part of the reason I would want this.

This feature would also make protocols looks like a type of thing that I wish Swift had, but the ship has sailed on, which are traits/mixins (whether or not you think those are good things is another topic). Granted you can use protocols like they are now in Swift to achieve the same result, you just have to add the property requirements in the concrete types. However, allowing method definitions inside a protocol body, to me, makes it looks more like protocols should allow stored properties. You're defining an implementation of the required method, why not allow defining a default value for a property in the protocol as well? And at that point you've basically got a mixin.

1 Like

Why would this (default implementations within protocols) work any differently than what we have today?

An extension or protocol cannot declare additional storage requirements for a type because extensions and protocols can be augmented onto a type retroactively, and possibly by a third party.

You're describing a code structure that you like—and you bring up valid reasons for it—but nowhere does the Swift compiler actually enforce that you declare your protocol extensions separately from your struct (the way e.g. Haskell does it). So, Swift lets you choose whichever way you prefer to structure your code; there might be valid reasons to do it otherwise (e.g. to be able to see all the protocol conformances at one glance without scrolling through a huge file or having to look at a list of files).

Similarly, Swift never enforces that you put e.g. all your private declarations below your public or internal ones etc. Generally, I think that it should be up to the developer to choose the best way to structure their own code. Otherwise we can go back all the way to Pascal with very strict structural requirements.

Other than flexibility of structure, I also think that the current situation is not really intuitive. It would already be much better if you could say:

protocol Doer {
    func doSomething()
    func doSomethingTwice()
}

defaults Doer {
    func doSomethingTwice() {
        doSomething()
        doSomething()
    }
}

Because I don't really understand how a default implementation can be considered an extension.

10 Likes

It's interesting, because I feel exactly the opposite way. I look at a protocol today and I have no idea which requirements I need to implement, because it's not obvious which ones have defaults—roughly, which ones are requirements and which are "just" customization points. I'm not sure this would solve the problem, though, because plenty of requirements are satisfied with implementations in constrained extensions, of which there may be more than one.

Therefore my conclusion is the same: this is not worth adding to the language. Focusing on distinguishing "this method may implement a requirement" from "this is just a regular statically-dispatched method" in protocol extensions seems more interesting to me. (Having an implements-style keyword for this is something I'm much more amenable to.)

28 Likes

To me it looks like the needs of Jordan and Ben are to be addressed by the tools (code folding, more contextual actions when clicking on a method name, etc.) or by the documentation. Syntax alone can't address them all. It reminds me of one pitch by Erika Sadun (IIRC - can't find it) about extending the syntax with keywords that would make explicit if a method is a default implementation, a customization point, part of a protocol adoption, etc.

EDIT: why do people like swiftdoc.org? Because of this (scroll down to "Default Implementations" and below)

EDIT2: the quality of swiftdoc.org has nothing to do with any grouping in extensions, and would be the same when this pitch gets implemented. That's why I mention it here.

I can’t make a formal reply atm, but the proposal that @gwendal.roue is referring to is here: swift-evolution/XXXX-role-keywords.md at a260a33ca39676b41e0436c4dccdb78441308c13 · erica/swift-evolution · GitHub

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