Swift enum property *without* initializing the enum case with an associated value?

I need to link a simple String or Int property and an enum case.

The cases will have associated values, but I want to access this property on a per-case, "static" manner prior to initializing the enum case with an associated value.

The value of the property will not rely on the associated value

enum MyEnum {
    case valueNone
    case valueString(String)
  //case valueString(Int) // Invalid redeclaration of 'valueString'
    case valueExpensive(CustomDataTypeThatsExpensiveToConstruct)
    
    var associatedValueTypeString: String {
        switch self {
        case .valueNone:         return "none"
        case .valueString(_):    return "string"
        case .valueExpensive(_): return "custom"
        }
    }
    
}

struct CustomDataTypeThatsExpensiveToConstruct { }

Note that case valueString(...) can only be declared once, with a single associated value Type. So theoretically a valueString(_).property type accessor would not be ambiguous.

It works fine for enum cases without associated values (I realize the case is being "initialized"):

let type1: String = MyEnum.valueNone.associatedValueTypeString // ok

But fails when you try to reference an associated value case without passing an associated value:

let type2: String = MyEnum.valueString(_).associatedValueTypeString
//Error: '_' can only appear in a pattern or on the left side of an assignment

Is there any construct in Swift that allows for "static" properties on enum cases, that would allow for property access without initializing the case with a valid associated value?

1 Like

Probably not what you want, but:

enum MyEnum {
    case valueNone
    case valueString(String?)
    case valueExpensive(CustomDataTypeThatsExpensiveToConstruct?)
    
    var associatedValueTypeString: String {
        switch self {
        case .valueNone:         return "none"
        case .valueString(_):    return "string"
        case .valueExpensive(_): return "custom"
        }
    }
    
}

struct CustomDataTypeThatsExpensiveToConstruct { }

print(MyEnum.valueNone.associatedValueTypeString)           // "none"
print(MyEnum.valueString(nil).associatedValueTypeString)    // "string"
print(MyEnum.valueExpensive(nil).associatedValueTypeString) // "custom"
1 Like

Yeah, looking for a non-Optional associated value version.

I found another reference to this ability:

Basically:

Bear with my inaccurate analogy here, because it might provide a better explanation of what I'm talking about:

In an enum, when a case has no associated values, you can think of values of that case behaving like a type in that there's only one version of it that can exist: the thing itself. There's no range of values that can be represented by such an enum case; there's just one value.

Now, once you introduce associated values, the concept of an enum case suddenly changes: it no longer represents a single thing, it represents a potential range of things. In other words, realized versions of those case values stop acting like types and start acting like instances of a type.

Anywhere else in Swift where something acts like a value, there's an equivalent metavalue type that can also be represented in a Swift variable.

It seems to me it's quite common in software development that when you model specific instances of things, you also need a way to represent the general form of those specific things.

(You could off course use some custom generic wrapping type other than Optional but I guess that's not what you're looking for either.)

It might be interesting to note that cases with associated values are functions from their associated value's type to the enum type (MyEnum in this case):

print(type(of: MyEnum.valueNone)) // MyEnum
print(type(of: MyEnum.valueString)) // (String) -> MyEnum
print(type(of: MyEnum.valueExpensive)) // (CustomDataTypeThatsExpensiveToConstruct) -> MyEnum

So you could implement what afaics is essentially your "static" property associatedValueTypeString (only as a free function) like this:

func uglyWorkaround<T>(_ v: T) -> String {
    switch v {
    case is (MyEnum):
        return "none"
    case is (String) -> (MyEnum):
        return "string"
    case is (CustomDataTypeThatsExpensiveToConstruct) -> (MyEnum):
        return "custom"
    default:
        preconditionFailure("Unhandled argument")
    }
}

print(uglyWorkaround(MyEnum.valueNone)) // none
print(uglyWorkaround(MyEnum.valueString)) // string
print(uglyWorkaround(MyEnum.valueExpensive)) // custom

But your underlying concrete problem can probably be better solved using some totally different approach.

2 Likes

That's a cool approach. But it looks like it would fail to uniquely identify cases if they happen to use the same associated value Type, regardless of the case label?

case valueString(String)
case someOtherCaseWithString(String)

From the OP, it looked like you had the case, and you wanted to know its associated value's type (if any). Given two cases with the identical associated value, it will (and I guess should) simply return their respective associated vaue's type (which will be identical).

Without an associated value, a case (with an associated value) is nothing but a function (sort of like a type constructor).

What I mean is that the function will return the same value for any case that has that associated value Type?

case oneCase(String)
case twoCase(String)

....

case is (String) -> (MyEnum): return 1 << 1 // for `oneCase`
case is (String) -> (MyEnum): return 1 << 2 // for `twoCase`, but never matches due to above case

My suboptimal workaround was to extend all the nested enums (and/or associated types) to conform to CaseIterable like this:

extension CaseIterable {
    static var anyCase: Self {
        if let firstCase = allCases.first {
            return firstCase
        } else {
            preconditionFailure("🔴 - There should be at least one case in a type that conforms to CaseIterable")
        }
    }
}

extension String: CaseIterable {
    public static var allCases = ["anyCase"]
}

extension Int: CaseIterable {
    public static var allCases = [0]
}

After that you can do this (your example refactored):

enum MyEnum: CaseIterable {
    static var allCases: [MyEnum] = [.valueNone, .valueString(.anyCase), .valueExpensive(.anyCase)]

    case valueNone
    case valueString(String)
    case valueExpensive(Action.TimerAction)

    var associatedValueTypeString: String {
        switch self {
        case .valueNone:
            return "none"

        case .valueString:
            return "string"

        case .valueExpensive:
            return "custom"
        }
    }
}

let type1: String = MyEnum.valueNone.associatedValueTypeString
let type2: String = MyEnum.valueString(.anyCase).associatedValueTypeString