Idiomatic solution to "work around" inability to override protocol conformance added by an extension

Hi,

I have recently participated in a discussion where an interesting problem was brought up.
In enterprise-Scala-all-around code the ability to override default typeclass implicits seems to be high-regarded.

The problem (in Swift terminology):

Imagine a library that provides a protocol (e.g. Serializable) and a function (e.g. serialize). The library also provides default implementation of the protocol for some known types, including but not limited to, builtins such as Array. Moreover, in the latter case default implementations are Array.Element specific.

Now consider a case where the end user decides, that the defaults are not good enough. Worse, the library is a readonly dependency, so no patching or ad-hoc fixes.

In Scala this problem is naturally addressed by adding additional implicit with appropriate type signature. The compiler then selects the implicit (in case if there are multiple matching the signature) from the closest namespace.

In Swift, I originally speculated, one could expect similar behavior, i.e. one could just write another extension Array: Serializable where Element == Int in the main module (outside of the library), maybe sprinkle it with some keywords and then trust the compiler to pick it as being closer then one from the library.
I was obviously wrong as Swift forbids multiple protocol conformances.


I'm interested in knowing what would be the idiomatic approach for this case that would match the behavior in terms of flexibility.


Relevant links:

There are two choices.

The first is to define a new protocol, MySerializable, to which you conform types appropriately. This can delegate to Serializable in most cases, and diverge where necessary.

The second is to encapsulate Array in a new type, which you conform to Serializable, and then use that type everywhere you require it.

In both cases the only loss of flexibility is that if a third module was involved that used both Serializable and Array, that module’s behaviour would be unchanged. I think that’s probably a good thing: at a certain point we’re getting into the realm of arbitrary monkeypatching, which is not necessarily a programming model I’d want to encourage.

2 Likes

This is an interesting question and the more I think about it, the more I see it being a "thing". IMO it makes sense to accept that SomeModule uses a specific extension to make an object conform to a protocol while I want to have a different extension to conform to the same protocol in AnotherModule.

This would work nicely if we could namespace extensions somehow I think. Which AFAIK isn't possible in Swift.

Did I understand you correctly:

// --- Library ---

protocol LibrarySerializable {
    func toString() -> String
}

extension Array: LibrarySerializable where Element == Int {
    func toString() -> String { return String(describing: type(of: self)) }
}

func serialize(_ object: LibrarySerializable) {
    print(object.toString())
}


// --- Application ---

protocol MySerializable: LibrarySerializable {}

extension Array: MySerializable where Element == Int {
    func toString() -> String { return String(String(describing: type(of: self)).reversed()) } // 🛑 Invalid redeclaration of 'toString()'
}

serialize([1, 2, 3])

But this code does not compile when declared in the same module. When split into a binary and a static library it does compile, but app's definition is ignored.

Is it arbitrary if namespaces are well defined? After all you do not "patch" an arbitrary node, you patch a leaf (you may want different "defaults" in different parts of your application).