0

I'd like to have a GenericThing with a template parameter that is any type that can sensibly be converted to and from a string.

// ConvertsToAndFromString is a made up protocol here – what should I use instead?
struct GenericThing<Id: ConvertsToAndFromString> {
}

I should then be able to use GenericThing with any type that has a reasonable encoding as a string. For example, it should work for Int, String (well, dah), and ideally, any RawRepresentable where the RawValue itself will convert to and from a string.

Example:

enum Tubbies: String {
  case dipsy
  case laalaa
  case po
}

// I'd like to be able to do this.
let genericThing = GenericThing<Tubbies>

I can't see how to easily do this.

I was hoping I could use LosslessStringConvertible instead of my made up ConvertsToAndFromString.

I tried this, and it works for Int and such. But it doesn't work for Tubbies. I couldn't see a way to make all RawRepresentable where RawValue: LosslessStringConvertible also conform to LosslessStringConvertible.

Benjohn
  • 13,228
  • 9
  • 65
  • 127

2 Answers2

4

This is how you extend RawRespresentable to be conditionally LosslessStringConvertible depending on its RawValue:

extension RawRepresentable where RawValue: LosslessStringConvertible {
    init?(_ rv: RawValue) {
        self.init(rawValue: rv)
    }

    var description: String { return self.rawValue.description }
}

Here it is in action:

struct GenericThing<Id: LosslessStringConvertible> {

}

enum Tubbies: String, LosslessStringConvertible {
    case dipsy
    case laalaa
    case po
}

let genericThing = GenericThing<Tubbies>()
print(Tubbies.po is LosslessStringConvertible) // => true
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • :-) Brilliant, thank you! … I should also ask, is there any obvious risk with extending all `RawRepresentable` in this way? I'm grinning because I got this answer slightly after asking the question, but I'm glad you confirmed it and you got the first answer in! – Benjohn Nov 07 '19 at 15:14
  • 1
    @Benjohn You could run into issues is somebody else defined the same conformance in a library you've imported, but generally, that kind of thing is rare enough that I don't let it hold me back from writing the code I would like. "cross that bridge when you get there", you know? – Alexander Nov 07 '19 at 15:47
  • `var description: String { .init(rawValue) }` – Leo Dabus Nov 07 '19 at 18:43
  • @LeoDabus Eh, I'm not a fan. I like the clarity that simply being `description` being passed through, uneffected – Alexander Nov 07 '19 at 18:46
  • https://developer.apple.com/documentation/swift/customstringconvertible/1539130-description – Leo Dabus Nov 07 '19 at 18:47
  • @LeoDabus I'm not sure I get your point. Are you trying to point out the part where it says it's discouraged to be called? – Alexander Nov 08 '19 at 01:03
  • Yes if you can avoid it why not – Leo Dabus Nov 08 '19 at 01:03
  • Because avoiding it comes to no benefit here, and only makes this code more indirect and less clear. The whole point of this adapter pattern is to forward onto the existing `LosslessStringConvertible` APIs, one of which is the `description` property inherited from `CustomStringConvertible`. Calling the `String` initializer works, but it's ultimately point is to call `description` for you. Your code is still depending on that happening, even though it doesn't express it explicitly. So you're no less coupled to `description`, you've just hidden the coupling behind `String.init` – Alexander Nov 08 '19 at 12:26
  • This seems to break the `CodingKey` protocol used for `Codable` objects. It causes the compilation error: `Type 'Test.CodingKeys' does not conform to protocol 'CustomStringConvertible'` – Joe Aug 10 '21 at 22:48
  • @Joe I don't really know what you're talking about. Please open a new post and add more detail. – Alexander Aug 11 '21 at 00:42
0

Had an issue in Swift 5.2 at least where extending RawRepresentable was causing CodingKeys to fail compiling.

public extension RawRepresentable where RawValue: LosslessStringConvertible {
    init?(_ rv: RawValue) { self.init(rawValue: rv) }
    var description: String { rawValue.description }
}

struct Test: Codable {
    public var test: String
    enum CodingKeys: String, CodingKey { // Error: Type 'Test.CodingKeys' does not conform to protocol 'CustomStringConvertible'
        case test = "foo"
    } 
}

My workaround was to instead explicitly add conformance using the same strategy, it requires being able to change the enum but allows CodingKeys to compile.

public protocol LosslessStringConvertibleEnum: LosslessStringConvertible, 
    RawRepresentable where RawValue: LosslessStringConvertible {}

public extension LosslessStringConvertibleEnum {
    init?(_ rawValue: RawValue) { self.init(rawValue: rawValue) }
    var description: String { rawValue.description }
}

enum Tubbies: String, LosslessStringConvertibleEnum {
    case dipsy
    case laalaa
    case po
}
Joe
  • 3,664
  • 25
  • 27