In Swift, how can I specify a typealias that restricts RawRepresentable to String?

7.9k views Asked by At

I'm trying to define a protocol that requires an enum with raw value String to be implemented.

I don't believe that it's currently possible to enforce the use of enum, and I'm not sure I really care as long as somewhere down the line I can call fromRaw() and receive a String.

So, I'm trying to maintain the brevity of the following while restricting Beta to be an enum where the raw value is a String:

protocol Alpha {
    typealias Beta: RawRepresentable
}

struct Gamma: Alpha {
    enum Beta: String {
        case Delta = "delta"
    }
}

struct Eta<T: Alpha, U: RawRepresentable where T.Beta == U> {
    let alpha: T
    let beta: U
    init(alpha: T, beta: U) {
        self.alpha = alpha
        self.beta = beta
        println("beta is: \(beta.toRaw())")
    }
}

let gamma = Gamma()
Eta(alpha: gamma, beta: .Delta) // "beta is delta"

The problem with the above is that other raw values are allowed, and therefore this is valid:

struct Epsilon: Alpha {
    enum Beta: Int {
        case Zeta = 6
    }
}

let epsilon = Epsilon()
Eta(alpha: epsilon, beta: .Zeta) // "beta is 6"

To address that I'm currently doing this:

protocol StringRawRepresentable: RawRepresentable {
    class func fromRaw(raw: String) -> Self?
}

protocol Alpha {
    typealias Beta: StringRawRepresentable
}

struct Gamma: Alpha {
    enum Beta: String, StringRawRepresentable {
        case Delta = "delta"
    }
}

// Type 'Epsilon' does not conform to protocol 'Alpha'
struct Epsilon: Alpha {
    enum Beta: Int {
        case Zeta = 6
    }
}

struct Eta<T: Alpha, U: StringRawRepresentable where T.Beta == U> {
    let alpha: T
    let beta: U
    init(alpha: T, beta: U) {
        self.alpha = alpha
        self.beta = beta
        println("beta is: \(beta.toRaw())")
    }
}

let gamma = Gamma()
Eta(alpha: gamma, beta: .Delta) // "beta is delta"

Is there a way that I can declare the typealias differently in the original example to restrict RawRepresentable to String?


Update

Specifying U: RawRepresentable where U.Raw == String seemed hopeful, so I gave that a try:

protocol Alpha {
    typealias Beta: RawRepresentable
}

struct Gamma: Alpha {
    enum Beta: String {
        case Delta = "delta"
    }
}

struct Eta<T: Alpha, U: RawRepresentable where T.Beta == U, U.Raw == String> {
    let alpha: T
    let beta: U
    init(alpha: T, beta: U) {
        self.alpha = alpha
        self.beta = beta

        // Execution was interrupted, reason: EXC_BAD_ACCESS (code=EXC_I386_GPFLT).
        println("beta is: \(beta.toRaw())")
    }
}

let gamma = Gamma()
Eta(alpha: gamma, beta: .Delta) // "beta is delta"

struct Epsilon: Alpha {
    enum Beta: Int {
        case Zeta = 6
    }
}

let epsilon = Epsilon()
Eta(alpha: epsilon, beta: .Zeta) // Error only occurs when this is executed

While this technically prevents using anything other than a String, I'm looking for a compile-time constraint and this appears to be causing a runtime exception.

I'd also prefer that this be enforced by the protocol if possible rather than consumers needing to check that .Raw == String

4

There are 4 answers

0
Rikkles On

Let's look at our options here. First, it's (as of Xcode 6 beta 5) a well known restriction that we cannot specify enum type constraints in the easy and expected way. Second, you need something very clear: to be able to call fromRaw(String). And third, you want a compiler error. I'd say your best bet is to write a protocol to do exactly that, and push down to the consumer the requirement to ensure s/he gives you a fromRaw(String). In this case, here's what I would do, simplifying your second code snippet:

protocol Alpha {
    typealias Beta: RawRepresentable
    func fromRaw(raw: String) -> Beta?
}

struct Gamma: Alpha {
    enum Beta: String {
        case Delta = "delta"
        case Omega = "omega"
    }
    func fromRaw(raw: String) -> Beta? {
        return Beta.fromRaw(raw)
    }
}

struct Eta<T: Alpha, U: RawRepresentable where T.Beta == U> {
    let alpha: T
    let beta: U
    init(alpha: T, beta: U) {
        self.alpha = alpha
        self.beta = beta
        println("beta is: \(beta.toRaw())")
    }
}

let gamma = Gamma()
let a = Eta(alpha: gamma, beta: .Delta) // "beta is delta"
println(gamma.fromRaw("delta"))  // Optional(Enum Value)
println(gamma.fromRaw("omega")!) // Enum Value

Philosophically this is imho more aligned with your needs: You say "I want something not only RawRepresentable, but also a fromRaw(String). Figure out how you give it to me". The Gamma struct is the simplest example, where the consumer specifics his enum, and then says "ok, I can give you my standard fromRaw() because it works.

1
jimejim On

Just to add onto this since it's a bit older, your updated example works in swift 2+ now and will complain at compile time that .Zeta is ambiguous unless it's a String type.

You can also put the check in a pattern match for a protocol extension. As an example:

extension SequenceType where Generator.Element:RawRepresentable,
                         Generator.Element.RawValue == String {
    func toStringArray() -> [String] {
        return self.map { $0.rawValue }
    }
}
0
Ash On

This should now be possible. For example, the following protocol allows a class to define its own enumeration of 'input' parameters conforming to string:

protocol AttributeContainer {
   associatedtype InputKey: RawRepresentable where InputKey.RawValue: StringProtocol
   func set(value: Any?, for inputKey: InputKey)
}

This could be used as follows:

class MyClass: AttributeContainer {

    enum AttributeKey: String {
       case attributeA, attributeB, attributeC
    }

    func set(value: Any?, for inputKey: InputKey) {
        // Handle the setting of attributes here
    }

}

This is similar to how Apple handles CodingKey in the Codable protocol. I find it useful when doing things like storing arbitrary class types in a database.

1
DesignatedNerd On

A colleague and I talked this out and in Swift 2.0/2.1, you can do this with a protocol: https://gist.github.com/designatednerd/5645d286df0ce939714b

Tried it out in an app I'm working with, works like a charm. :)