Imagine that you have a set of integers and you want to override the default implementation of CustomStringConvertible
for a Set
to use set-builder notation. You might try to do this with an extension like the following:
extension Set: CustomStringConvertible where Element == Int {
var description: String {
let values = self.map({ "\($0)" }).joined(separator: ", ")
return "{\(values)}"
}
}
Unfortunately, this fails to compile with the following error:
Conformance of 'Set' to protocol 'CustomStringConvertible' conflicts with that stated in the type's module 'Swift' and will be ignored; there cannot be more than one conformance, even with different conditional bounds
Well, it looks like that won't work because Set
already conforms to CustomStringConvertible
.
Okay, fine. We can still provide our own implementation of description
without declaring a (conflicting) conformance to CustomStringConvertible
:
extension Set where Element == Int {
var description: String {
let values = self.map({ "\($0)" }).joined(separator: ", ")
return "{\(values)}"
}
}
Now the compiler's happy, so let's kick the tires a bit:
let set: Set<Int> = [7, 3, 15, 31]
String(describing: set) // prints "[7, 3, 15, 31]"
Wait, what? Where are our curly braces?
set.description // prints "{7, 3, 15, 31}"
Oh, there they are. Hmm…
I know that static dispatch is used to call methods that are defined in a protocol extension rather than in the protocol itself, but description
is defined in the original CustomStringConvertible
protocol, so shouldn't init(describing:)
use dynamic dispatch to call our own custom implementation?
All hope is not lost yet, though. We can also try defining an empty SetBuilderNotationRepresentable
protocol:
protocol SetBuilderNotationRepresentable: CustomStringConvertible {}
Next, we'll make sets of integers conform to this new protocol:
extension Set: SetBuilderNotationRepresentable where Element == Int {}
Finally, we'll provide our own implementation of init(describing:)
:
extension String {
init<Set>(describing instance: Set) where Set: SetBuilderNotationRepresentable & Sequence {
let values = instance.map({ "\($0)" }).joined(separator: ", ")
self.init("{\(values)}")
}
}
Now all that's left is to cross our fingers, and…
String(describing: set) // prints "{7, 3, 15, 31}"
Success!!
In all seriousness, though, this seems a bit excessive just to replace a pair of square brackets with curly brackets. Am I missing something here, or is this really the most "elegant" way to provide our own implementation of CustomStringConvertible
for a collection?