Enums with multiple raw values

Several times I have had situations where the cases of an enum, conceptually, have both and integer value and a string value that they correspond to. This can be accomplished by having one be the raw value and the other be a computed property, but the choice of which should be the raw value is arbitrary, and the computed property is boilerplate.

A basic example:

enum Numbering: Int
{
  case first = 1
  case second = 2

  var name: String
  {
    switch self {
      case .first: return "first"
      case .second: return "second"
    }
  }
}

The obvious thing to try here would be a tuple:

enum Numbering: (Int, String)
{
  case first = (1, "first")
  case second = (2, "second")
  
  var number: Int { return self.rawValue.0 }
  var name: String { return self.rawValue.1 }
}

...but that doesn't work. The basic problem seems to be that 1) enum raw values must be literals, and a tuple of literals doesn't count, and 2) the raw value type must be Equatable, and a tuple of equatables doesn't count.

Are there other good solutions for this? How much interest is there in addressing those tuple limitations?

1 Like

It just doesn't work for the automatic synthesis of RawRepresentable. You can manually implement RawRepresentable for this pretty easily.

enum Tuples {
    case one
    case two
}

extension Tuples: RawRepresentable {
    init?(rawValue: (Int, String)) {
        switch rawValue {
        case (1, "one"): self = .one
        case (2, "two"): self = .two
        default: return nil
        }
    }
    
    var rawValue: (Int, String) {
        switch self {
        case .one: return (1, "one")
        case .two: return (2, "two")
        }
    }
}

There have also been a variety of discussions around tuples and their conformance to various protocols. But really what you'd need here is to make the automatic synthesis for RawRepresentable more flexible, like removing the literal requirement.

Yeah, with a bit more fiddling I came up with basically the same thing, although I don't think you'd ever be initializing it with the whole tuple. More likely, you'd have one or the other individual type. So you'd have the initializer required by RawRepresentable, plus another initializer for each type in the tuple. The end result is that the individual raw types are equal citizens, but there's still a lot of boilerplate :frowning_face:

You could do:

enum Number: Int {
  case first = 1
  case second = 2

  var name: String {
    return String(describing: self)
  }
}

let number: Number = .first

print(number.rawValue) // 1
print(number.name) // first
1 Like

Yeah, I forgot about String(describing:). Definitely good when the case name is the string you want.

I figured out how to put all that boilerplate into a protocol:

protocol IntAndString: RawRepresentable where RawValue == (Int, String)
{
  static var invalidInt: Int { get }
  static var invalidString: String { get }
}

extension IntAndString
{
  var int: Int { return self.rawValue.0 }
  var string: String { return self.rawValue.1 }
  
  static var invalidInt: Int { return -1 }
  static var invalidString: String { return "" }

  init?(int: Int) { self.init(rawValue: (int, Self.invalidString)) }
  init?(string: String) { self.init(rawValue: (Self.invalidInt, string)) }
}

extension IntAndString where Self: CaseIterable
{
  init?(rawValue: (Int, String))
  {
    if let match = Self.allCases.first(where: { $0.rawValue.0 == rawValue.0 || $0.rawValue.1 == rawValue.1 }) {
      self = match
    }
    else {
      return nil
    }
  }
}


enum Numbering: CaseIterable
{
  case first
  case second
}

extension Numbering: IntAndString
{
  var rawValue: (Int, String)
  {
    switch self {
      case .first: return (1, "first")
      case .second: return (2, "second")
    }
  }
}

let a = Numbering(int: 2)
let b = Numbering(string: "first")
1 Like

So this isn't what I would call "safe", but it's possible to do with a custom backing Type:

struct NumberingValue: RawRepresentable, Codable, ExpressibleByStringLiteral, Equatable {
    typealias StringLiteralType = String

    let value: Int
    let name: String

    var rawValue: String {
        return "\(value),\(name)"
    }
    init?(rawValue: String) {
        let components = rawValue.components(separatedBy: ",")
        guard components.count == 2 else {
            return nil
        }
        self.value = Int(components[0])!
        self.name = components[1]
    }

    init(stringLiteral rawValue: String) {
        self.init(rawValue: rawValue)!
    }
}

Which allows:

enum Numbering: NumberingValue{
    case first = "1,first"
    case second = "2,second"

    var value: Int { return rawValue.value }
    var name: String { return rawValue.name }
}

print(Numbering.first.value) //prints 1
print(Numbering.first.name) // prints "first"

If desired you could make NumberingValue use a Tuple internally, and I would put plenty of documentation/errors around it for production code.

I'm hoping some day we can define enum backing-values without requiring Literals. I think that would allow using any RawRepresentable to back an enum.