Default Implementation in Protocols

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

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.