Swift enum custom raw type not expressible by different literals

Viewed 651

I was reading through the wonderful blog post by Jon Sundell where he is trying to demonstrate how one can use custom raw values with Swift Enums.

I had a play around with his code and made up a bit of a contrived example to see how robust the Enums with custom raw types are.

I wondered if I can make a type, instance of which can be used as a raw value whilst being expressible by both String and Integer literals. When I implemented ExpressiblebyIntegerLiteral the String part did not work the way I expected and I'm not quite sure why.

Here is the code (the question follows the code):

import Foundation

struct Path: Equatable {
    var string: String
    var int: Int
}

enum Target: Path {
    case content
    case resources
    case images = "resources/images"
}

extension Path: ExpressibleByIntegerLiteral, ExpressibleByStringLiteral {
    init(stringLiteral: String) {
        string = stringLiteral
        int = 0
    }

    init(integerLiteral: Int) {
        string = ""
        int = integerLiteral
    }
}

if let stringTarget = Target(rawValue: "content") {
   print("stringTarget: \(stringTarget)")
}

if let intTarget = Target(rawValue: 1) {
   print("intTarget: \(intTarget)")
}

What I expected the above code to produce is both stringTarget and intTarget being initialized to appropriate Enum cases, however, the stringTarget one turns out to be nil.

If you remove the conformance to ExpressibleByIntegerLiteral protocol (and the appropriate block of code which initializes intTarget), the stringTarget automagically gets initialized as I expected: into Target.string case.

Could someone please explain to me what is going on here?

1 Answers

Solving your Question

What I expected the above code to produce is both stringTarget and intTarget being initialized to appropriate Enum cases, however, the stringTarget one turns out to be nil.

They aren't nil. They are this: ""

This happens because both the .content and .resources cases are not explicitly defined by a String. And because of this, they both take the ExpressibleByIntegerLiteral route, and are hence defined as this ""

init(integerLiteral: Int) {
    string = "" // see
    int = integerLiteral
}

Solved for Int

Use this fancy method in place of IntValue(rawValue: 1):

func IntValue(_ this: Int) -> String? {
    return Target(rawValue: 0) != nil ? String(describing: Target(rawValue: 0)!) : nil
}

Solved for String

First, conform your enum to CaseIterable, like so:

enum Target: Path, CaseIterable {

Next, use this fancy method in place of Target(rawValue: "content"):

func StringValue(_ this: String) -> String? {
    return Target.allCases.contains { this == String(describing: $0) } ? this : nil
}

Truly solved for String

Now, I've removed a crucial bug where case foo = "bar" can be found both as 'foo' or 'bar'. If you don't want this, use this code instead:

func StringValue(_ this: String) -> String? {
    var found: String? = nil
    _ = Target.allCases.filter {
        if let a = Target(rawValue: Path.init(string: this, int: 0)) {
            found = String(describing: a)
            return this == String(describing: a)
        }
        found = String(describing: $0)
        return this == String(describing: $0)
    }
    return found
}

Custom Raw Values for Enums

Here's a quick tutorial:

I am wondering if enum can conform it's rawValue to the ClosedRange struct, just like it can conform to String.

enum Foo: ClosedRange<Int> {
    case bar = 1...4
}

Obviously this is not an option, since 1...4 is not a literal

This seems to work:

enum Foo: ClosedRange<Int> {
    case foo = "1...3"
    case bar = "1...4"
    func overlaps(_ with: Foo) -> Bool { return self.rawValue.overlaps(with.rawValue) }
}
extension ClosedRange: ExpressibleByStringLiteral {
    public typealias StringLiteralType = String
    public init(stringLiteral value: String) {
        let v = value.split(separator: ".")

        switch Bound.self {
        case is Int.Type: self = (Int(v[0])! as! Bound)...(Int(v[1])! as! Bound)
        default: fatalError()
        }
    }
}

It allows you to do this:

print(Foo.foo.overlaps(Foo.bar))

Side Note: My attempt allows for non-unique rawValues (SR-13212) which is a shame. But I'm not thinking that is fixable:

enum Foo: ClosedRange<Int> {
    case foo = "1...3"
    case bar = "1...4"
    case bar = "1...04" // Duplicate, but Swift allows it.
}

Related