Enum case Typed-signature / interpolation

What is the best way to make a enum case Typed-signature / interpolation?

enum TestEnum {
    case caseWithoutAssociatedValues
    case bar2(value: Int)
    case baz(val: Int, String)
    case baz(val: UInt, bal: (String, ty: Double))
    case caseWithAssociatedValues(Int, String, Double)
    case caseWithOptionalFunc(func: ((Double, Double) -> Double)?)
    case caseWithFuncOptionalReturn(((Double, Double) -> Double?))
    case caseWithOptionalFuncOptionalReturn(((Double, Double) -> Double?)?)
}

Expected output:
"caseWithoutAssociatedValues"
"bar2(value: Int)",
"baz(val: Int, String)",
"baz(val: UInt, bal: (String, ty: Double))",
"caseWithAssociatedValues(Int, String, Double)"
"caseWithOptionalFunc(func: Optional<(Double, Double) -> Double>)"
"caseWithFuncOptionalReturn((Double, Double) -> Optional)"
"caseWithOptionalFuncOptionalReturn(Optional<(Double, Double) -> Optional>)")

Currently I use reflection to solve this task. But there is a plan to drop reflection metadata to reduce app binary size as there are no other reasons for reflection metadata.
Another option is to use macros, but this new feature unavailable for older OS versions.

Is there any other options?

Besides hand-coding something to spit out the string you’re looking for based on the enum case (like a computed property on the enum), reflection and macros are pretty much the available options.

You could probably rig up some package plugin too to generate the strings at build-time, but I doubt it’d be worth the effort unless the enum is massive and changes extremely quickly.

I'd say hardcode it, along with a test that will ensure proper string for all exemplar case values – if there's a mismatch after a change (e.g. due to refactoring, etc) the test will fail either during compilation or during runtime so you'd be alerted that the test needs the corresponding change.

extension TestEnum {
    var string: String {
        switch self {
            case let .caseWithOptionalFunc(func: f):
                let s = "\(TestEnum.caseWithOptionalFunc(func: nil))"
                    .replacingOccurrences(of: "nil", with: "\(type(of: f))")
                precondition(s == "caseWithOptionalFunc(func: Optional<(Double, Double) -> Double>)")
                return s
            default:
                fatalError("TODO")
        }
    }
}
1 Like

BTW, there's something odd with that enum. Distilling the oddness down:

enum Foo {
    case baz(UInt)
    case baz(bal: Int)
}

func foo(e: Foo) {
    switch e { // 🛑 Switch must be exhaustive
        case let .baz(bal): print(bal)
        case let .baz(bal: bal): print(bal) //🔶 Warning: Case is already handled by previous patterns; consider removing it
    }
}

And if that's not odd enough for you consider the next one:

enum Bar {
    case baz(Int)
    case baz(bal: Int)
}

switch Bar.baz(1) {
    case let .baz(bal): print(bal)
    // ✅ compiles fine?!
    // 🤯 that's exhaustive?!
    // 💣 Runtime crash: Fatal error: unexpected enum case while switching on value of type 'Bar'
}
2 Likes

Yes, this is a compiler bug: [SR-10077] Switching over duplicate enum case names won't compile · Issue #52479 · apple/swift · GitHub

My be there are other options to handle the uniqueness of enum instance? I mean .bar2(value: 7) and .bar2(value: 999) shout be treated as same. I need something that get stable hashValue for all instances of case .bar2(value:) no matter what associated value is. Say if .bar2(value: 7) is already contained in storage, insertion of .bar2(value: 999) should replace .bar2(value: 7). Hashable can't be used for this purpose.

Something like 'case + params name' property is enough, but I have no idea how ti implement it without macros or reflection.

One idea I've explored is using String(describing:).

enum AEnum {
  case a(b: Double)
}

String(describing: AEnum.a(b: 4.4))

The output of this code is "a(b: 4.4)" which can be reduced to "a(b:)". This is nice.
But this approach is broken if client will implement CustomStringConvertible for AEnum. And again, I don't know how to call default STL implementation for enum description to workaround situation if CustomStringConvertible is implemented.

Will that work for you?

struct AEnumWrapper: Hashable {
    let value: AEnum
    var discriminator: Int {
        switch value {
            case .caseWithoutAssociatedValues: 0
            case .bar2(value: _): 1
            ...
        }
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.discriminator == rhs.discriminator
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(discriminator)
    }
}

Or without a wrapper, changing EQ/Hash of the AEnum itself, if that's appropriate.

The problem is that enum itself is declared on the client side, so

  • something autogenerated is needed
  • I can't rely on client's implementation, because something like this can happen:
switch value {
    case .caseWithoutAssociatedValues: 0
    case .bar2(let value): return value
    ...
}

What is the end goal?

It's possible but tricky to get enumeration's discriminator.

// MARK: DO NOT USE THIS CODE
// use with enums only
func discriminator<Enum>(_ e: Enum) -> Int {
    withUnsafeBytes(of: e) { p in
        let n = p.count
        let tag = p.load(fromByteOffset: n - 1, as: UInt8.self)
        let first = p.load(fromByteOffset: 0, as: UInt8.self)
        return tag == 7 ? 1000 + Int(first) : Int(tag)
    }
}

let examples: [TestEnum] = [
    .caseWithoutAssociatedValues,
    .bar2(value: 0),
    .baz(val: 0, ""),
    .baz(val: 0, bal: ("", ty: 0)),
    .caseWithAssociatedValues(0, "", 0),
    .caseWithOptionalFunc(func: nil),
    .caseWithFuncOptionalReturn({ _, _ in 0 }),
    .caseWithOptionalFuncOptionalReturn(nil)
]
for (i, e) in examples.enumerated() {
    print("examples[\(i)],", terminator: " ")
    print("discr =", discriminator(e), terminator: ", ")
    print("val =", e)
}

outputs:

examples[0], discr = 1000, val = caseWithoutAssociatedValues
examples[1], discr = 0, val = bar2(value: 0)
examples[2], discr = 1, val = baz(val: 0, "")
examples[3], discr = 2, val = baz(val: 0, bal: ("", ty: 0.0))
examples[4], discr = 3, val = caseWithAssociatedValues(0, "", 0.0)
examples[5], discr = 4, val = caseWithOptionalFunc(func: nil)
examples[6], discr = 5, val = caseWithFuncOptionalReturn((Function))
examples[7], discr = 6, val = caseWithOptionalFuncOptionalReturn(nil)

On the framework side instances of enum with associated value are stored.

On the client side a enum will be declared. If new enum instance of the same case is added / inserted then new instance replace previous. Both instances of the same enum case should not be stored.

This might be the solution, I will check and test it. Thanks a lot.