A payload dump case for raw-valued enumeration types?

Just a random thought. Look at:

enum MyEnum: Int {
    case zero, one, two, three
}

what if we could do coverage over all of Int's valid bit combinations with:

enum MyEnumTwo: Int {
    case zero, one, two, three
    @dumpEnum others(Int)
}

The payload must match the RawValue. It shouldn't have the value of any of the singular cases, but we probably can't enforce that yet. Anyway, there's no extra tag; the implementation is still a veneer over Int, just the values not covered by the singular cases are covered but the .others case.

Another random thought: maybe it could cascade:

enum MyAuxillary: Int {
    case four = 4, five, six, seven
}

enum MyEnumThree: Int {
    case zero, one, two, three
    @shadowDump auxiliary(MyAuxillary)
    @dumpEnum others(Int)
}

Here, non-overlapping enumeration types with the same implementation type can sub-type each other.

I don't know if this has been done before, or has a computer-science concept behind it. Is it even a remotely good idea?

Is the idea that the following would happen?

let e = MyEnumTwo(rawValue: 5) // .others(5)
let f = MyEnumTwo.others(3) // .others(3), or
                            // invalid, if compiler can enforce mutual-exclusivity

Something like that, at least for e. I'm not sure about f. If we do a read, we would get .three; that's the whole point of staying a pure RawValue without a tag. But it smells too close to C's undefined behavior to be Swift-y enough to allow.

I am then unsure what you mean by that statement. Could you provide examples of what should work, what shouldn't work, and what values we would get for the former cases?

These should be banned:

var a: MyEnumThree
a = .others(1)  // Should be .one
a = .others(5)  // Should be .auxillary(.five)

//...

switch a {
case .others(2):  // Hopefully the compiler can catch this.  Use .two instead.
    break
case .others(let x) where x < 4:  // Can the compiler realize this?
    break
case .others(let x) where x < 8:  // Or this?
    break
//...
}

but that could only be done when the compiler can see the attempted raw values. Would we need the constexpr apparatus first? Do we add code to call preconditionFailure for runtime mismatches? If the last two cases couldn't be noticed, the run-time behavior would have to let them go through.

It's worth noting that non-@objc Swift enums don't use the raw type as the representation; your first example

enum MyEnum: Int {
    case zero, one, two, three
}

still uses a one-byte representation. But there's still some implementation benefit to your dumpEnum not using an extra tag for the "others" case, and it would work with @objc enums too. (@Joe_Groff has brought this idea up before and I think there's a Radar for it somewhere.)

shadowDump seems a little more iffy to me, mainly because I think there's a lot of ways it could go wrong. I'd rather "just" implement non-public cases, which (while it also needs plenty of design) is a generally applicable feature anyway.

FWIW, I've done things similar to this before:

enum MyEnumTwo: RawRepresentable {
    case zero, one
    case others(Int)

    var rawValue: Int {
        switch self {
        case .zero: return 0
        case .one: return 1
        case .others(let value): return value
        }
    }

    init?(rawValue: Int) {
        switch rawValue {
        case 0: self = .zero
        case 1: self = .one
        default: self = .others(rawValue)
        }
    }
}

I often use an .other or .empty case with a custom init. This seems like it simplifies that design pattern and allows better ergonomics around value-backed enums, so I think I'm in favor of something like this!

The verbiage probably needs some work. @dumpEnum sounds like an action not a declaration, and is kind of unclear about what it's for.

How about @else or @default? It takes advantage of existing terms and seems to fit what's happening well.

I will mention that this struct does have the same behavior except for not caching the answer:

struct RawOr<Wrapped: RawRepresentable>: RawRepresentable {
  var rawValue: Wrapped.RawValue
  init(rawValue: Wrapped.RawValue) {
    self.rawValue = rawValue
  }
  var wrappedValue: Wrapped? { Wrapped(rawValue: rawValue) }
}

A sufficiently smart compiler (augmented with sufficient levels of @inlinable) would probably be able to treat this the same as the enums above, as long as the compiler can see the implementation of MyEnum.init(rawValue:).

2 Likes