swiftgenericsstructconditional-statements

Swift custom struct: string interpolation not picking up custom description


In the following Swift code:

import Foundation

struct MySet<T: Hashable>: CustomStringConvertible {
    let set: Set<T>
}

extension MySet {
    var description: String {
        return "\(self.set)"
    }

}

extension MySet where T: Comparable {
    var description: String {
        return "\(self.set.sorted())"
    }
}

let intSet: Set<Int> = [6, 3, 4, 9, 12, 49, -1, 44, -1000, 1000]
let myIntSet = MySet(set: intSet)
print("myIntSet: \(myIntSet.description)")
print("myIntSet: \(myIntSet)")
print("myIntSet: ", String(describing: myIntSet))

I'm trying to create a custom description (for the CustomStringConvertible protocol) that is different depending on whether the elements of the set are Comparable. The output of the above is:

myIntSet: [-1000, -1, 3, 4, 6, 9, 12, 44, 49, 1000]
myIntSet: [9, -1, 3, 1000, 6, 4, 12, 44, 49, -1000]
muIntSet:  [9, -1, 3, 1000, 6, 4, 12, 44, 49, -1000]

As you can see, if I explicitly print myIntSet.description, I get the desired output, which is a sorted list. But if I use string interpolation on the struct itself, or String(describing: myIntSet), I get the description from the first (unconditional) extension.

Can anyone explain why this is happening? How do I get string interpolation and String(describing: ...) to use the desired description?


Solution

  • Every protocol requirement can only have one witness. In this case the witness for CustomStringConvertible.description is the one declared in the first MySet extension. The description declared in the second extension only hides the description declared in the first extension, and is not a witness to the protocol requirement.

    When you access description on a comparable MySet, you cannot access the hidden implementation, but when description is accessed through the CustomStringConvertible protocol (indirectly by string interpolation or String.init(describing:)), only the protocol witness can be accessed.

    This means that all the behaviour you want must be put in the description declared in the first extension. For example, you can add your own protocol that allows you to provide a custom description, then check if self conforms to that protocol in description.

    protocol CustomSetDescribable {
        var customDescription: String { get }
    }
    
    struct MySet<T: Hashable>: CustomStringConvertible {
        let set: Set<T>
    }
    
    extension MySet {
        var description: String {
            (self as? any CustomSetDescribable)?.customDescription ?? "\(self.set)"
        }
    
    }
    
    extension MySet: CustomSetDescribable where T: Comparable {
        var customDescription: String { "\(set.sorted())" }
    }