On the wisdom of adding members to standard library protocols

sometimes, i just really can’t think of anywhere else to put a function, except as an extension on a standard library protocol:

extension Collection where Element == URI.Vector?
{
    // ["foo", "..", "bar"] -> ["bar"]
    func normalized() -> (components:[String], fold:Int)
    {
    }
}

but this just feels wrong, because the part of my brain that screams “extending standard library protocols is very bad practice” is telling me it needs to go anywhere but on Collection.

refactoring it into an initializer on a type like FoldedPathComponents also feels wrong, because the second tuple return value is meant to be discarded most of the time.

how do you deal with this dilemma?

I do not see a dilemma.

The entire purpose of protocols is to let people write algorithms against them.

That is exactly what you are doing.

7 Likes

This is an entirely fine practice, and it is guaranteed to continue working as standard library overloads are designed to always lose out to third-party overloads.

3 Likes

there are many problems with extensions on external protocols:

  • they are less discoverable, because you cannot find them from “URI.Vector”, you must search in Collection’s namespace.

  • if the associatedtype constraint was less strong (e.g., String instead of URI.Vector?), the extension could collide with other peoples’ extensions.

  • and probably the most important (in my view), such protocol extensions are hard to document (see docs for swift-url, which have to call out API like String.percentEncoded(using: PercentEncodeSet) -> String in an ad-hoc paragraph)

    from the perspective of a package author, protocol extensions are “dark API” — users can’t really discover them while using their normal tooling/workflow.

Agree, we do need better documentation tooling and a disambiguation syntax. Both have come up recently (as well as earlier) and I think are tractable problems. It’s still worth emphasizing that it’s an explicitly supported use case though.

4 Likes

Make it a global function?

 func normalized(_ v: [URI.Vector?]) -> (components: [String], fold: Int) { ... }

or a static function on, say, URI.Vector.

Spitballing here, but is it a viable workaround for the documentation issues to extend an alias instead?

public typealias CollectionExtensions = Collection
extension CollectionExtensions {
  func normalized() -> (components: [String], fold: Int) {
    // ...
  }
}
1 Like

Nice. Note, you'd have to specify the where clause as well:

extension CollectionExtensions where Element == URI.Vector?

Interestingly, it is not possible to specify where clause in the type alias:

typealias SomeCollection = Collection where Element == URI.Vector? // Error 🤔
1 Like

Why do you think that? It's one of the foundational features of Swift.

If a function describes a completely generic algorithm, perfectly applicable to any Collection where Element == SomeGeneralGlobalType, then I think it's perfectly fine to declare it in an extension. You can control visibility to a particular file or module with the access control modifiers, it if makes sense for your code base.

On the other hand, if we're dealing with, for example, collections of specific business entities, then I think it's probably better to model the collection itself as a business entity, with its own functions.

2 Likes

This is a valid point, but it's tractable in its own specific context (documentation tooling), and doesn't go against the architectural and maintainability aspects of protocol extensions.

I'm not sure about this point though:

what do you mean? That people would want to implement that same extension function in a different way? As far as I can tell, if they do it in their own module, their implementation will override the imported one, but it would be nice to have better disambiguation tools for protocol extensions.

BTW these doesn't apply just to protocols – also to concrete types – but Swift encourages generic programming and reusable algorithms via parametric polymorphism, and protocol-oriented programming is a huge part of that.

3 Likes

I don’t think it’s a bad practice as long as you don’t break the semantics of Collection or any other SSL protocol. We should avoid extending types we don’t hold — but the standard library is there, providing a stable interface and available for everyone to use and extend to some extent. If the requirement Element == URI.Vector is held by your library, it should be pretty okay. (And yes, extending with Element == String is really bad and we should keep it at least internal).

For SwiftURL, the main concern for me is its interoperability around Foundation, which is now overly used and frequently mistaken as a stable interface while it actually isn’t.

2 Likes