Introduce an "ExpressibleByCase" protocol (to facilitate struct-based enums with payload)

Wouldn't your handler for synthetic cases also need to have a 30-way decider? And with less static information to help you.

The synthetic-case declaration doesn't match any current protocol requirement syntax. But since I suggested synthetic cases for both struct and class types, all three nominal kinds of types would support cases, and therefore we can add to protocols the ability to declare case requirements! Remember that nominal types would be able to add additional cases, so methods extending the protocol would need "@unknown default" conditions in their switch statements. Would that alleviate your concerns about moving around protocol extensions? I don't know what the parenthetical part of that quote means.

...

In another reply, I mention we could add an opaque EnumerationCase standard library type and a global func case<T>(of: T) -> EnumerationCase? to extract tags if you really want to have tag-only comparisons. With the type, I can retire the "match" block for synthetic cases and introduce a special method to get a case in hopefully less than linear time.

struct myStruct {
    /* Insert the actual members here */

    case mySingularCase { /*...*/ }
    case myPayloadCase(Int, whatever: Double) { /*...*/ }
    /* Insert more synthetic cases here */

    var as case: EnumerationCase? {
        if myFirstMember == 6 {
            return \.mySingularCase
        } else if mySecondMember.isEmpty {
            return \.myPayloadCase
        } /*...*/ else {
            // This will trigger the `@unknown default` condition on a `switch`.
            return nil
        }
    }
}

switch statements will generally require an @unknown default condition unless the compiler can see that every state of the product type is covered by a case.


I wasn't here at the start of Swift, so maybe those discussions did happen back then?

1 Like

Hey Daryl

I'm not so sure that is really the case.

From what I can see, the rationale behind enums-with-payload is that they kinda serve as adjectives. Eg: rather than hardcode hundreds of "shape" cases, create: shape(dimensions:1), shape(dimensions:2)... shape(dimensions:n).

But, in reality, payloads are much more useful (given Swift being strongly typed) as nouns. Eg: shape1D(x), shape2D(x,y), shape3D(x,y,z)

I don't know if I'm making any sense here, but, essentially, I think, the majority of the time, the only part of the enum the user wants to test is the case itself not the payload. In fact, using a switch statement to test the payload is, to my eye, messy. It is easier to read/write/understand code that does not deal with extracting/casting the payload within the switch statement itself (within the switch test, I mean, as opposed to handling it in statements within the body).

With the struct model I am currently using (which inspired me to post this proposal) it is possible to init with the payload as a Tuple**. This means, the init does little work other than store it as a member variable, and later, at the call-site, retrieve the stored Tuple. However, as you point out, the payload is indeed type-erased.

While this means the programmer has to manually check what the type of the payload is, if we built it into Swift, with the 'case' keyword, there would be many situations where, I imagine, the IDE could check what the payload of a variable has to be. Eg.

if mycase == .point2D { 
    /* IDE could refer to the cases declared in the struct, and 
       code-complete "let xy = mycase()" to suggest 
       "let xy: (x:Double,y:Double)? = mycase()" */ 
}

** (for every situation except where the case payload would need storage Tuple with a single and labelled value)

This is easy enough to do without changing the language, but I’m guessing this isShape() function is the ā€œmessyā€ part you want to avoid. How would you expect it to know if it is a shape or not in your perfect world?

enum Shapeish {
    case notAShape
    case shape1D(x: Int)
    case shape2D(x: Int, y: Int)
   
    func isShape() -> Bool {
        switch self {
        case .shape1D(x: _), .shape2D(x: _, y: _): return true
        default: return false
        }
    }
}

Sorry, my previous comment wasn't very clear.

Every single thing I want to change about Swift enums has a workaround. It's not even that the workarounds are particularly obscure. What bothers me is that to make an enum human-friendly requires a lot of extra code. Without workarounds, how nice are these enums to use at the call-site? Not very (eg: if case let).

With my previous comment, I was just speculating that perhaps the reason payloads aren't (in my opinion, anyways) pleasant to work with, is that they are designed to be compared, and matched, against each other.

In practice, though, enums are the best-suited datatype in Swift to work with collections of disparate types. That being the case, it ought to be easier to get at their payloads. As it stands, aside from the famously ugly "if case let" syntax, Swift pushes you into storing them in a temporary variable (if there are alternatives to "if case let" I don't suppose they are well known).

Considering enums-with-payloads are the go-to data type for storing disparate types, the language should treat them with more respect.

I know it is a pain point for new devs, and I feel like I’ve had to re-lookup the syntax to access a case’s associated value a dozen different times, so I can appreciate that feeling. What I don’t understand is how the pitch would help. How does having cases in a struct make it easier to access the payloads associated to the cases in a way that still reasonably maintains the strong typing that’s intrinsic to Swift? A hypothetical payload member that gives you direct access to a case’s payload would only reasonably return Any? so you’d already have to know what it was or pattern match the case so it seems like we’ve just gone full circle.

1 Like

It would be stored as Any?, but could be accessed as T?, and that's a significant difference. Eg:

func add( _ d: Double ){
    // do something
}

guard mycase == .doub else { return }
add(  mycase() ?? 0.0 )

This is possible by making the struct @dynamicCallable, and giving the getter a generic return type, which tries to cast from Any to T, returning nil if it fails. So now, we don't need to explicitly write "as?" everywhere.

It does that indirectly by (and I screwed this up in my description at the top of the page) allowing you to store the enum payload as a Tuple. Once you have that, you can make create a payload getter (I like dynamicCallable for the getter) to retrieve the payload tuple (or value type) as T.

Again, the other thing I want from enums (or struct-based enums) is the ability to use both .mycase and .mycase(MyPayload) in the same enum.

I mentioned this a bit earlier in the thread, but I'd actually prefer we implement all these changes with existing Enums, but making a big source-breaking change like that would probably cause disruption, and never be accepted :(

Is it source breaking to allow .option and .option(value) if it’s already guaranteed there’s not two cases named option? That seems additive, so nobody should have code that breaks.

1 Like

I'm not sure, tbh, but I hope it's not source-breaking. Such a change would make me awfully happy :)

It's legal to reference a case name without its associated values. It behaves as a constructor for the case.

enum E {
    case a(Int)
}

let aCons = E.a

let b = aCons(3)

print(b) // a(3)
2 Likes

Thanks! Amazingly, despite all my interest in payload enums, it never occurred to me to look into that.

So... any change here would be source-breaking :(

I assume the fact that a change here would be source-breaking is what Avi was driving at. Before someone else suggests it: no, it's useless as a workaround, for tons of reasons:

let p: Param = .mycase // illegal (so can't use Enum name as a shorthand)

// Can't move equality to a protocol extension without writing
// a custom == func for each new enum payload signature   
func == ( lhs: (/*?*/)->Param, rhs: Param  ) -> Bool