Stored properties inside enums

Some time ago, I made a simple LL Parser using enums to build the AST. But I ran into the problem of having to declare extra common fields inside every case of all my enums to store information. For instance, the position field:

enum Expression : AST {
    case Number(value: Float, position: (Int, Int))
    case BinaryOperation(op: Operator, e1: Expression, e2: Expression, position: (Int, Int))
    case UnaryOperation(op: Operator, e: Expression, position: (Int, Int))
}

Now that I've returned to my rusty old project, my curiosity drove me to read many discussions about adding stored values to enums. I disagreed with some of them, but others caught my attetion. Especifically the following comment by @Jumhyn in a thread made by @hisekaldma, who had the exact same problem as I did.

The above proposal with that particular example looks pretty solid and clean to me. I understand people trying to keep enums and structs separeted. But by treating these stored properties just like functions and computed property are treated nowday, I don't see any syntax or technical issue.

Why are these proposals still being rejected? In my opinion, it would add extra power to Swift's already powerful enums.

In my opinion it's because if stored properties were allowed, then they would be forced to have default values. There are no other cases in the language in which the user is forced to choose a default value.

The way to go with the current situation is to use a struct:

struct Expression: AST {
    enum Value {
        case number(value: Float)
        case binaryOperation(op: Operator, e1: Expression, e2: Expression)
        case unaryOperation(op: Operator, e: Expression)
    }

    let value: Value
    let position: (Int, Int)
}

Exhaustive switches are simply done over the .value property. That would be the only difference.

7 Likes

I think it may be possible to treat stored properties in enums as a syntax sugar for having a common field in all cases + computed property for reading/writing it.

Example:

enum Color {
    case rgb(r: CGFloat, g: CGFloat, b: CGFloat)
    case white(white: CGFloat)
    case hsl(hue: CGFloat, saturation: CGFloat, luminosity: CGFloat)
    var alpha: CGFloat
}

as a shortcut for this:

enum Color {
    case rgb(r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat)
    case white(white: CGFloat, alpha: CGFloat)
    case hsl(hue: CGFloat, saturation: CGFloat, luminosity: CGFloat, alpha: CGFloat)
    
    var alpha: CGFloat {
        get {
            switch self {
            case let .rgb(_, _, _, a): return a
            case let .white(_, a): return a
            case let .hsl(_, _, _, a): return a
            }
        }
       set {
           switch self {
           case let .rgb(r, g, b, _): self = .rgb(r, g, b, newValue)
           case let .white(w, _): self = .white(w, newValue)
           case let .hsl(h, s, l, _): self = .hsl(h, s, l, newValue)
           }
       }
    }
}
3 Likes

I think this is a reasonable idea (though I also think “just wrap your enum in a struct” is also a reasonable solution), but what happens if there’s a name collision? What if an enum case has an associated value label with the same name as the var? And is there syntactic sugar for initialization and for the other direction to pretend the extra stuff isn’t tacked on? Can I, in your example, do case let to read the value ignoring the synthesized alpha?

let w = Color.white(white: 0.5) //Error without sugar
if case let .white(wValue) = w {
    wValue //Expecting this to be a CGFloat value,
           //but without some sugar this is actually
           //going to be a (CGFloat, CGFloat) tuple
           //capturing both white: and alpha: values.
}

I’m no expert on compiler magic, but this seems more complicated than first glance.

Sorry, I wasn’t clear about that. My idea was that after expanding syntax sugar, it behaves like a regular enum.

let w = Color.white(white: 0.5, alpha: 1.0)
if case let .white(wValue, _) = w {
    wValue
}

That’s simpler, but quickly gets out of control if you add multiple vars and suddenly your simple case is now littered with underbars. Anything beyond a single var probably means wrapping in a struct is a less awkward solution.

Been thinking this through, and maybe the better way to think about this would be a "Common Associated Value" rather than a "Stored Property". The goal is to easily access a value that's included in every case, not to extend/add anything per-se.

I'm sure it's been referenced in previous discussion, but Kotlin's sealed class supports this kind of thing (With the caveat that Swift's enum varies in many ways):

sealed class MyClass(val myValue: String) {
    class SubType(val title: String): MyClass("Test")
    class OtherType(val order: Integer, myValue: String): MyClass(myValue)
}
val instance: MyClass = MyClass.Subtype("It works!")
// Since `myValue` is part of `MyClass` it can be accessed without determining the subclass
val value = instance.value

It seems like the more Swifty approach would be code generation to support the convenience. (Forgive me if this has been discussed/proposed already)

e.g. given your enum...

enum Color {
    case rgb(r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat)
    case white(white: CGFloat, alpha: CGFloat)
    case hsl(hue: CGFloat, saturation: CGFloat, luminosity: CGFloat, alpha: CGFloat)

    var alpha: CGFloat
}

/// Full generated version 

enum Color {
    case rgb(r: CGFloat, g: CGFloat, b: CGFloat, alpha: CGFloat)
    case white(white: CGFloat, alpha: CGFloat)
    case hsl(hue: CGFloat, saturation: CGFloat, luminosity: CGFloat, alpha: CGFloat)
    
    var alpha: CGFloat {
        get {
            switch self {
            case let .rgb(_, _, _, a): return a
            case let .white(_, a): return a
            case let .hsl(_, _, _, a): return a
            }
        }
    }
}

If needed it could be opt-in with a protocol, but I haven't come up with a reasonable name/functionality that makes sense. GeneratedCommonValues is explicit, but doesn't really seem to fit in with other protocols with generation.

Side-note, not sure if set would/should be generated since IMO it "feels" wrong to have in enums by default, but it could also easily be added in separately.

Would it be terrible to have enum stored properties act almost as aliases for named arguments? So given...

enum {
    case rgb(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)
    case hsv(hue: CGFloat, saturation: CGFloat, volume: CGFloat, alpha: CGFloat)
    case monochrome(value: Bool)

    let alpha: CGFloat?
}

In this case the alpha property is set to match the case value for .rgb(...) and .hsv(...) but is nil for monochrome(...). One downside is that a given alias could only have a single type, although you could sort of get around that with another enum.

A Huge Yes. Any update for this feature? Definitely need this。

indirect enum Expr {
    case binary(left: Expr, operate: Token, right: Expr)
    case call(callee: Expr, parenthesis: Token, argument: [Expr])
    case grouping(Expr)
    case literal(Literal)
    case unaray(operate: Token, right: Expr)
    case variable(name: Token)
    case assign(name: Token, value: Expr)
    case logical(left: Expr, operate: Token, right: Expr)

   var depth: Int = 0
}

Thanks a lot

1 Like