Enum case values' names

i like using enums with raw types:

enum StringEnum: String {
    case one
    case two
    case three
}

let v: StringEnum = ...
v.rawValue // string here

in some cases i have to use a different raw type for some reason (e.g. Int), but still want to have names for case values:

enum IntEnum: Int {
    case one
    case two
    case three
}

let v: IntEnum = ...
v.rawValue // int value here
v.stringValue // a hypothetical new method that would return
              // "one", "two" or "three" in this particular case
              // will also work if there is no raw type

i can do this manually but would rather avoid that boilerplate:

extension IntEnum {
    var stringValue: String {
        case .one: return "one"
        case .two: return "two"
        case .three: return "three"
    }
}

or in a specific case of 0, 1, 2... enum:

extension IntEnum {
    var stringValue: String {
        ["one", "two", "three"][rawValue]
    }
} 

do you think it is worth considering this new feature as an addition to swift at some point? or maybe that was already considered?

or is there a way to do this without a boilerplate in a generic fashion with mirror API?

2 Likes

You can achieve this by simply doing: String(describing: IntEnum.one). (There are some nuances to this, but generally it works.)

3 Likes

wow... every time i tried it in the past i was getting something stupid like the name of the type itself.. thanks!

Except where the enum conforms to a protocol which vends its own description. Then you lose this ability. A notable example is CodingKey.

I very much would support a language feature for this. I’ve had to incorporate sourcery into projects specifically to solve this problem.

1 Like

You can also do this via runtime introspection, e.g. using Wes Wickwire's Runtime library:

enum IntEnum: Int {
  case one, two, three
}

import Runtime // https://github.com/wickwirew/Runtime

let metadata = try typeInfo(of: IntEnum.self)
let caseNames = metadata.cases.map(\.name)
print(caseNames) // → ["one", "two", "three"]

This also works with @karim's CodingKeys case:

import Runtime // https://github.com/wickwirew/Runtime

struct Person: Codable {
  var name: String
  var age: Int

  static var codingKeys: [String] {
    let metadata = try! typeInfo(of: CodingKeys.self)
    return metadata.cases.map(\.name)
  }
}

print(Person.codingKeys) // → ["name", "age"]

The library inspects the type metadata (which contains the case names) that is embedded in the binary. If I understand correctly, this should be safe on ABI-stable platforms (Apple) because the metadata layout will not change in a breaking manner. On other platforms the metadata layout might change, which means the library would have to be adapted for the new compiler version.

Here is a dump of the metadata value from the first example. It should give you a good idea of the kind of data the library makes available:

▿ Runtime.TypeInfo
  - kind: Runtime.Kind.enum
  - name: "IntEnum"
  - type: TypeMetadataIntrospection.IntEnum #0
  - mangledName: "IntEnum"
  - properties: 0 elements
  - inheritance: 0 elements
  - size: 1
  - alignment: 1
  - stride: 1
  ▿ cases: 3 elements
    ▿ Runtime.Case
      - name: "one"
      - payloadType: nil
    ▿ Runtime.Case
      - name: "two"
      - payloadType: nil
    ▿ Runtime.Case
      - name: "three"
      - payloadType: nil
  - numberOfEnumCases: 3
  - numberOfPayloadEnumCases: 0
  - genericTypes: 0 elements
3 Likes

the built-in String(describing: xxx) doesn't work (correctly) for enums imported from Obj-C like URLRequest.CachePolicy (aka NSURLRequestCachePolicy).

does runtime introspection method work correctly for those?

1 Like

Not with this library. And I don’t think the binary contains the case names for enums imported from C/Obj-C. C-based enum cases are just integer constants from the perspective of the compiler.

import Foundation
import Runtime

dump(try typeInfo(of: URLRequest.CachePolicy.self))

Output (notice that the cases array is empty):

▿ Runtime.TypeInfo
  - kind: Runtime.Kind.enum
  - name: "NSURLRequestCachePolicy"
  - type: __C.NSURLRequestCachePolicy #0
  - mangledName: "CachePolicy"
  - properties: 0 elements
  - inheritance: 0 elements
  - size: 8
  - alignment: 8
  - stride: 8
  - cases: 0 elements
  - numberOfEnumCases: 6
  - numberOfPayloadEnumCases: 0
  - genericTypes: 0 elements
1 Like

but compiler knows what i mean when i write .useProtocolCachePolicy so in theory it can translate this to a "useProtocolCachePolicy" string accessible in runtime somehow.

if i can do this monkey job:

extension URLRequest.CachePolicy {
    static let stringValues = [
        URLRequest.CachePolicy.useProtocolCachePolicy.rawValue:
            "useProtocolCachePolicy",
        URLRequest.CachePolicy.reloadIgnoringLocalCacheData.rawValue:
            "reloadIgnoringLocalCacheData",
        URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData.rawValue:
            "reloadIgnoringLocalAndRemoteCacheData",
        URLRequest.CachePolicy.returnCacheDataElseLoad.rawValue:
            "returnCacheDataElseLoad",
        URLRequest.CachePolicy.returnCacheDataDontLoad.rawValue:
            "returnCacheDataDontLoad",
        URLRequest.CachePolicy.reloadRevalidatingCacheData.rawValue:
            "reloadRevalidatingCacheData"
    ]
    var stringValue: String {
        URLRequest.CachePolicy.stringValues[rawValue]!
    }
}

or this:

extension URLRequest.CachePolicy {
    var stringValue: String {
        switch self {
        case .useProtocolCachePolicy:
            return "useProtocolCachePolicy"
        case .reloadIgnoringLocalCacheData:
            return "reloadIgnoringLocalCacheData"
        case .reloadIgnoringLocalAndRemoteCacheData:
            return "reloadIgnoringLocalAndRemoteCacheData"
        case .returnCacheDataElseLoad:
            return "returnCacheDataElseLoad"
        case .returnCacheDataDontLoad:
            return "returnCacheDataDontLoad"
        case .reloadRevalidatingCacheData:
            return "reloadRevalidatingCacheData"
        }
    }
}

so shall be able the compiler... if nothing simpler exists then by just automatically and invisibly generating one of those fragments for me when i merely using "cachePolicy.stringValue" somewhere in the app, or when i'm expressing my intent more explicitly via some marker:

extension URLRequest.CachePolicy: HasStringValue {}

Yes, I believe the compiler could do this, but the Swift team decided not to implement imported enums this way, and I'm assuming they had good reasons. I'm not a compiler developer, so I can only speculate.

For one, every piece of metadata you include increases the size of the compiled binary.

Perhaps more importantly, C/Objective-C binaries don't contain this metadata, so Swift's Clang importer would have to somehow include extra metadata (parsed from the C headers) for imported types in the binary that links with the imported libraries. And then the imported type would somehow have to include a reference to this extra metadata so that the runtime can find it later. I assume this extra field would make the type layout of the imported type incompatible with C/Obj-C, requiring some kind of automatic bridging every time a value is passed between Swift and C/Obj-C code or vice versa.

1 Like

this is one of those things when you only pay this [increased binary size] tax if you opt-in to use that feature, be it using the manual code above or the autogenerated code.

then maybe it's easier to do the autogeneration (like above) separate to those existing mechanisms if it is indeed a major challenge to enhance those existing mechanisms to support this case. and should those existing mechanisms enhance in the future to support this case as well - the auto generator can be dropped without breaking source compatibility or with some minor deprecations (e.g. "Warning: HasStringValue protocol conformance is no longer needed").

This is pretty much it: C binaries don't contain this info, so every Swift library that might inspect an enum's name would have to emit their own copy of the metadata. Furthermore, we generally didn't want to have Swift emit additional strings that come from C code out of a concern for secrecy: people who write enums in C might pick names like "CurrentProcessorARM128" and trust that that name doesn't leak the existence of ARM128 processors into the final binary.

I think it'd be reasonable to have an opt-in feature in Swift for this—for instance, another magic protocol you could conform to and have the compiler synthesize the implementation of. It's kind of a niche feature, but there's not another way to do it without the boilerplate Mike's gone through.

6 Likes

This is essentially what I wound up with using sourcery... a CaseNamable protocol which produced a caseName property for all conforming enums, patterned after the existing CaseIterable

does sourcery support obj-c imported enums?