Enum with raw value of type T and one case with single associated value T

I'm sure this has been discussed already -- I just couldn't find it. I hope this is the right place to ask.

I was interested in knowing more about if it would be possible for Swift to represent an enum along these lines:

enum Flag : Int {
    case notAQuestion = 1010
    case duplicate = 77
    case undocumented(anyOtherValue: Int) // catches any other value and makes it accessible
}

Flag(rawValue: 77) // .duplicate
Flag(rawValue: 90210) // .undocumented(90210)

There would only be one single case with an associated value (and not backed by a predefined value), and only one single associated value of the raw value type. That case would then only be constructable using Flag(rawValue:) to disallow collisions.

Basically I often find myself in the situation where I want to map all known values (like documented error codes from a 3rd party API) to cases, and still be able to propagate and access any unforseen value. Right now I'd probably either just use the raw value type (let flag: Int) and lose some semantics, or add an extra property alongside it (let flagAsEnum: Flag?) to aid readability and exhaustiveness (best effort) when switching over the value, but that's not very clean.

I found some discussions and proposals about non-frozen enums and unknown cases in switches, but they seemed to focus on enums defined in other modules where not all cases are known by clients, as opposed to this where not all values are known when defining the enum. Is there a discussion about this somewhere?

Clarification:

3 Likes

One technique to accomplish something like this is to write a struct that is RawRepresentable for wrapping your raw type. For instance:

struct Flag : RawRepresentable, Equatable {
    var rawValue: Int
}

and then add static members:

extension Flag {
    static let notAQuestion = Flag(rawValue: 1010)
    static let duplicate = Flag(rawValue: 77)
}

It's pretty much like an enum, but with an open-ended list of values. You can even switch over such a type:

switch someFlag {
case .duplicate: break
case .notAQuestion: break
default: break
}

Maybe this is what you're looking for.

6 Likes

Thank you, that's an excellent suggestion that I have never thought about doing. I didn't know there was a shorthand for accessing static members the same way. It looks good.

The only thing I would miss is the exhaustiveness, i.e. whenever I add a new known case I will not be warned about missing switch cases in different parts of the code.

Maybe the use case I gave is a corner case that's not worth the effort, but it would still be interesting to know why it would/wouldn't be possible to represent a catch-all enum case behind the scenes.

Yeah, that's something I'd like to have too.

This is also a bit similar to "non-frozen" enums in SE-0192 where you could use @unknown default in a switch statement and then check the raw value. Unfortunately, this proposal only allows non-frozen enums for the standard library and interfaces imported from C or Objective-C.

You can always write a RawRepresentable conformance manually:

enum Flag {
    case notAQuestion
    case duplicate
    case undocumented(anyOtherValue: Int) // catches any other value and makes it accessible
}
extension Flag: RawRepresentable {
    private static let _notAQuestion = 1010
    private static let _duplicate = 77
    init(rawValue: Int) {
        switch rawValue {
        case Flag._notAQuestion: self = .notAQuestion
        case Flag._duplicate:    self = .duplicate
        default:                 self = .undocumented(anyOtherValue: rawValue)
        }
    }
    var rawValue: Int {
        switch self {
        case .notAQuestion:                           return Flag._notAQuestion
        case .duplicate:                              return Flag._duplicate
        case .undocumented(anyOtherValue: let value): return value
        }
    }
}
4 Likes

Another variant with slightly less code:

extension Flag: RawRepresentable {
    init(rawValue: Int) {
        switch rawValue {
        case Flag.notAQuestion.rawValue: self = .notAQuestion
        case Flag.duplicate.rawValue:    self = .duplicate
        default:                         self = .undocumented(anyOtherValue: rawValue)
        }
    }
    var rawValue: Int {
        switch self {
        case .notAQuestion:                           return 1010
        case .duplicate:                              return 77
        case .undocumented(anyOtherValue: let value): return value
        }
    }
}

(These ought to both compile down to the same thing anyway in optimized builds.)

2 Likes

Definitely some clever ways to do it with current Swift. It looks exactly like what I would have liked to have synthesized from my example.

Although 4*n of lines of code quickly grows out of proportion when done manually. Let's hope there aren't more than 10 error codes :slight_smile:

What I meant by "possible for Swift to represent" was whether the way enums are currently represented by the compiler could possibly allow for mapping all remaining values to one special case.

It's implementable, but I think there's a lot of code in the compiler right now that assumes "has a raw type" implies "no payloads", even if the payload is the same as the raw type. So it'd be some work (and a full proposal, of course).

Declaring a raw type just writes out the RawRepresentable conformance for you. It doesn't actually change the in-memory representation of the enum. So in theory it would be possible to teach the compiler to synthesize more elaborate code when the enum has payload cases.

Even if the "associated value" in this special case is just syntactic sugar for read-only access to the raw value? To me that would imply that there is no payload (if I haven't mixed up the terminology here), hence the assumption "no payloads" holds true. Sorry if I misunderstood.

Sorry, I'm not sure I follow. I might not be familiar enough with how Swift is implemented. I was under the assumption that e.g. changing the raw type from Int to String would change the in-memory representation. I don't see how I could read the .rawValue dynamically if it didn't.

In my mind, what I propose wouldn't really work if the enum had more than one payload case, since the associated value is just syntactic sugar for what I believed to be the in-memory representation of the enum, i.e. the raw value.

In other words, unless there is an additional mechanism that defines the mappings of the unused values, it would be impossible to determine which values should resolve to which "payload case".

I was perhaps a bit unclear in the original post: the enum case wouldn't actually have an associated value -- it would just look that way to make it clear that 1. it isn't predefined, and 2. it can be accessed the same way as if it had an associated value. It just can't be instantiated manually.

In your example--

enum Flag : Int {
    case notAQuestion = 1010
    case duplicate = 77
    case undocumented(anyOtherValue: Int) // catches any other value and makes it accessible
}

--.undocumented(anyOtherValue: 77) would be a representable value distinct from .duplicate. For this to be possible, the enum must have a memory layout distinct from that of the raw value.

Actually it wouldn't, since my example was qualified by:

I'm not sure that this would then make sense as an associated value, then.

Yes, I was a bit unclear in my OP. Here's a clarification:

Let me try to clarify what I meant. Suppose I write:

enum Foo : String {
  case a = "hi"
  case b = "bye"
}

Now the value Foo.a and Foo.b are not actually stored as strings; their representation is opaque. Rather, the presence of the raw type (: String above) is basically just shorthand for:

extension Foo {
  init?(rawValue: String) {
    if rawValue == "hi" {
      self = .a
    } else if rawValue == "bye" {
      self = .b
    }
  }

  var rawValue: String {
    switch self {
      case .a: return "hi"
      case .b: return "bye"
    }
  }
}

Note that if you declare an @objc enum, the representation is equivalent to the raw type (and there are further restrictions on what the raw type can be). However @objc enums cannot have associated values, either.

You're right that the compiler would need some unambiguous rule for mapping values of type T among the payload cases in the event there is more than one.

2 Likes

Since you still want to be able to get the value back out, parts of the compiler have to treat it like it has a payload and parts have to treat it like it doesn't. Again, this is implementable, but it's something that would require auditing all parts of the compiler that treat enums-with-raw-values differently from other enums.

(Slava's also right about @objc enums going a step further. This is clearly the right tradeoff for integer raw values, but for String raw values it might not be a good idea to have the run-time representation be a String rather than a tag of some kind.)

1 Like

Aah, of course! In hindsight it's obvious :slight_smile: I wouldn't want them stored as strings either...

1 Like

Got it. Since I was just enlightened by Slava about how they're opaque, my example doesn't feel as simple anymore. As you say, then I can't simply get the value back out, without additional storage.

While running the risk of showing complete ignorance to the inner workings of swift (and performance), I assume you wouldn't even suggest this unless there exists some efficient internal fixed-size representation of String? Or were you thinking of some problem other than performance?

Perhaps it's time I just go and read the implementation of String...

Strings are "fixed-size" in that they're made up of a retainable object pointer, which includes the buffer of actual character data, and an integer (slightly oversimplified, but that's basically it). The bigger problem is that they're not unique, so switching over them has to fall back to a series of string comparisons. (There are various optimizations the compiler can do here, but they'll never be as good as switching over a "normal" enum, which has a single integer tag value.)

1 Like