Extract Payload for enum cases having associated value

I would like that too, but it is not possible without some compiler help. As a reminder this solution with Mirror is implemented and proposed as trial to try the ergonomics and the feature itself. While it works well in most of the cases and gives the idea of what I am proposing (letting you try it on a playground), it is not intended to be the final solution we could achieve having compiler level help.

After finally having the chance to explore this code further, I have to say it's a very clever use of reflection! I found it easier to understand by getting rid of the protocol and implementing the bulk of it in a single function:

func extract<Root, Value>(
  case embedding: (Value) -> Root, from root: Root
) -> Value? {
  guard
    let (label, anyValue) = Mirror(reflecting: root).children.first,
    let value = anyValue as? Value
      ?? Mirror(reflecting: anyValue).children.first?.value as? Value,
    Mirror(reflecting: embedding(value)).children.first?.label == label
    else { return nil }
  return value
}

Such that:

enum Foo {
  case bar(Int)
  case baz(Int)
}

extract(case: Foo.bar, from: Foo.bar(1))
// Optional(1)
extract(case: Foo.bar, from: Foo.baz(1))
// nil

I think it's perfectly legitimate to introduce this kind of code to applications and libraries in order to solve problems that can't be solved using first-class constructs, but I do worry that introducing it to the standard library is the wrong solution to a problem that should be solved at the language-level. Specifically I am curious about the performance trade-offs, because this solution has to:

  • Reflect on the given root value (sometimes twice in nested fashion)
  • Instantiate a new value with the given enum initializer function
  • Further reflect on this new value in order to compare case names

Truly a clever way to solve a real deficiency in the language, but I'm hopeful we can push through a more efficient, language-level design that doesn't rely on runtime reflection or an opt-in protocol.

Further notes:

  • The protocol you define seems to be a "marker" protocol. Is it necessary to declare the interface in the base protocol and allow folks to make custom conformances? What would you be able to achieve with this? The code still seems to work if you delete these requirements and merely rely on the protocol extension.
  • The label variable doesn't seem to be used.
  • I'm not quite sure what the for case let in decompose is doing, so I simplified in my code above to be a single guard let on the first of children. Please let me know if I'm missing an edge case.
7 Likes

I‘d like to propose a different solution: what about extending the syntax of anonymous functions to enable pattern matching on its arguments, i.e.

{ case .foo(let val) in val }

This would pattern match the argument and in the matching case return the result of the function wrapped in .some (i.e. .some(val)) and in the non matching case return nil.

This would keep all the safety and power of pattern matching, including elegant handling of multiple parameters where I might only be interested in some of them and nestings, e.g.

{ case .foo(_, .baz(let x, _), let y) in x * y }

-Thorsten

1 Like

I totally agree with you. I pitched it with Reflection so you could try it. But the actual implementation is intended to be a detail. Like I said before I would prefer to see this without the use of Mirror, having the compiler help.

Yes, it is. Being a protocol someone could make a struct or a class CaseAccessible. In that case the code that today is in the extension is no longer valid. It would be perfectly fine if we had constraint like we do for class, for enum types protocol CaseAccessible: enum { ... } but we don't. :frowning:

I could throw a fatalerror in the function if the mirror tells me that it's not processing an enum value (totally doable) but I would prefer a compile check rather than a runtime error.

Anyway, I believe that opt-in protocol with these proposed functions are a good solution. Mirror isn't. It works for me, but I hardly believe it's the best we can do if we bring this feature on the language level. My point is that If I can do it with mirror, the compiler can do it better.

I use this code in a live application. I needed label to log events without payload. We can remove it.

It is a legacy remainder of how in previous versions of the language enum was mirroring. I'm using this approach since long time now. I can rewrite the function, thanks.

With that in mind I would still prefer an approach that mirrors how data is accessed and mutated on structs.

Your latest subscript design is approaching struct[keyPath: \.field], so may as well make it the same and offer dot-syntax access. I think taking syntax people are used to (dot-syntax, key paths) is preferable to introducing an overload (static enum functions as a key path-like syntax). I think the fact that I used "synthesis" worried folks, but compiler support for your design would effectively be the same: it's more about compiler support for syntax than the pollution of namespaces, etc. For example, the fields I suggest introducing could be accessed with a trailing ? to distinguish themselves, and existing optional-chaining could adopt this feature instead.

I think that with compiler support we can get rid of the initial Foo and have simple dot notation in event[case: .bar], like we are able to do already in pattern matching inside ifs and guards. This would be my goal.

I'm still very keen on having it as opt-in, to be free to extend Observable, Signal and Sequence to allow the writing of new operators to work with streams of enums. I would like a non opt in solution just if we could do something like extension ObservableType where Element: enum but we can't.

This overloads existing dot-prefix syntax with an additional meaning, which isn't going to be great for compile times.

You would be able to write the same kinds of extensions with generics and avoid the protocol, e.g. if .bar (I'd prefer \.bar or \.bar?) referred to an EnumKeyPath<Root, AssociatedValue>. And if these things worked like property access I don't think you even need to define your extensions because existing algorithms already do the work: observable.compactMap { $0.enumCase } or observable.compactMap(\.enumCase).

Because of this, I don't think we should penalize enums over structs. Structs don't require opt-in behavior to get ergonomic data access or key paths. Neither should enums.

I'm afraid the goal isn't easy to reach. Let me try to explain what I find difficult and really odd considering what I know of the language:

I don't know what this .bar is, actually. I can't make it fit in the bestiary of "things" Swift let us use in our programs, as defined by the grammar reference.

I mean that it looks to me that your goal requires the invention of a whole new kind of "thing" so that it can be made workable. Not only a "thing" that is internal to the compiler. A "thing" that exists in userland:

// Perform event[case: .bar] in two steps:
let myCase = ... // what do we write here? What is the type of this thing?
event[case: myCase]
if case .bar = enumCase { }

what is on the left side of the equal? I believe that this is where enum fails. The impossibility to give a name to the left side of this expression. we call it pattern, but there is nor formal representation of what an enum pattern is.

This is part of a statement. Statements and expressions occupy different spaces. There's no way to capture the .bar of a pattern match and use it in a later pattern match. However, when you can pass a .bar to your subscript solution, this is an expression, so it must refer to some kind of object/value.

1 Like

You don't really care, because you can't split this snippet. It's an unbreakable "atom of syntax", as far as we are concerned.

So we don't really have to wonder what is this .bar, just in case some user would want to extract it from the pattern matching snippet and ask "what is the type of the variable I can store this .bar in?".

This here is the main point that tells me to not approve this pitch, even if it presents some clever workaround to the huge holes we have today in the language for what concerns enums.

I really appreciated the effort that @GLanza put in adding mutability, this is 100% something I would add to an official proposal on this. Also, I'd admit that if we don't get a different solution that I'd consider better, I'm fine with this, it's definitely better than nothing.

But keypaths are, to me, the way. As an heavy user of keypaths, one that writes lots of wonderful generic code based on them, I can say that they are one of my favorite Swift features, and they're becoming more and more relevant to the language (see for example the glorious SE-0252).

Key paths are automatically defined for any property, computed or not, this is not an opt-in behavior, and shouldn't be. The only thing missing to enums are the boilerplate-y computed properties for cases, so everything would be easily solved if those where generated, and I still don't see any problem in having them in the namespace, because they would be simply considered "features" of enum instances.

The approach taken in the pitch, to refer to a case with its constructor, unfortunately makes it a little too cumbersome to use, because we'd always forced to refer the full constructor: in Swift the .something syntax is used if and only if the context expects an instance of a certain type, and .something is a static function on that type (thus, also an enum case constructor).

But the solution is super simple and right at hand: generate keypaths for enums, and use keypaths instead of the case constructor. This approach would be perfectly in line with the current Swift state of art and philosophy.

The difference here is that if case ... is a statement, not an expression. Swift clearly separates evaluated expressions (something that you would pass as a parameter to a function, for example) to statements, that you cannot pass around, and are not evaluated.

For example:

enum Foo {
  case bar(Int)
}

let foo: Foo = .bar(42)

switch foo {
case .bar:
  break
}

let doesntCompile: Foo = .bar

The .bar in case .bar is part of a statement and doesn't follow the same rules as expression, it doesn't need to be "something" itself.

The .bar in let doesntCompile: Foo = .bar won't compile because we need an expression that evaluates to "something" there. Taken as an expression, .bar should be a (Int) -> Foo function, but it's not defined on the (Int) -> Foo type, in fact:

let stillDoesntCompile: (Int) -> Foo = .bar

causes the compilation error type '(Int) -> Foo' has no member 'bar', because .bar is defined on the type Foo. There's no way out here.

1 Like

I want to have a less direct conclusion, and expose some its intermediate steps.

The code below can't compile, because nothing says that the .bar "thing" has to come from Foo:

let myCase = .bar // WAT?
enum[case: myCase]

What can we change to make it compile?

What about an explicit type information, which would help type inference? The code below doesn't compile today, but it could be made valid in a future compiler version. All the required information is there:

let myCase: (Int) -> Foo = .bar // Could be made to compile one day.
enum[case: myCase]

The type (Int) -> Foo is the only possible type that we can put at this place today, as long as .bar, an Implicit Member Expression remains defined as:

An "implicit member expression" is an abbreviated way to access a member of a type, such as an enumeration case or a type method, in a context where type inference can determine the implied type.

(Emphasis mine). The only known member named bar that we know is Foo.bar, typed (Int) -> Foo. It is the only one we currently have.

Can we invent a new bar member somewhere else? Yes we can, many above have tried, with the synthesis of various derived types from the base enum (see messages above). But those are other pitches.

So, (Int) -> Foo. And now we have a real problem. It has already been stated above, I don't invent it. The real problem is that (Int) -> Foo falls short in terms of type-safety. Would the [case:] subscript accepts any such function, and not only cases from the Foo enum, the proposal would sound too dangerous for many.

So... I think the the suit is too tightly cut. We don't have enough freedom. We actually can't really "make it compile somehow", because we face contradictions or undesired side effects.

I hope it would never be the case. While type inference is often fine, for such case, you can't know from where the .bar member come from without browsing the whole Module code.

The module can perfectly define a method like this:

enum Foo  {
   case misc
}

struct Bar {
   func bar(a: Int) -> Foo {}
}

let myCase: (Int) -> Foo = .bar

To know that, you must parse the whole module, which is not something anybody want, neither for compiler performance, nor for human readability of the code.

It could be made possible for this pitch precisely, if it were to be accepted and implemented, without having to browse the whole Module.

This is why I have looked for other, harder, issues.

I'm not sure it could be made to compile. I might be mistaken, but Foo.bar is not a "member" of the type (Int) -> Foo, but an "instance" of it. To make the thing compile an actual member should be defined, for example I think compilation would be successful if the we could write or synthesize the following code:

/// pseudoswift

extension (Int) -> Foo {
  static var bar: (Int) -> Foo {
    return Foo.bar
  }
}
1 Like

@ExFalsoQuodlibet and @Jean-Daniel, you are both right, and somewhat wrong. You are right because the current compiler can't turn .bar into (Int) -> Foo. But compiler can change! That's the whole point of Swift Evolution!

We can't answer @GLanza "this is impossible" when something actually is possible with a little effort. Bringing to the light what are the necessities and the contingencies of a proposal is the whole point of our conversations here.

This is why I consider the argument "type inference can't do this" as a weak one. I find other issues that appear, after the type inference step has been solved, much more concerning.

let myCase: (Int) -> Foo = .bar

.bar is a synthesized static func of Foo. Could the compiler check this?

Even if the compiler would check this, how could it disambiguate cases with the same payload type, or avoid users from feeding the [case:] subscript with any kind of (Int) -> Foo function? How can (Int) -> Foo alone contain enough information?

I know you have made a clever use of Mirror, which helps solving the case disambiguation. But how robust is this solution?

I think this is where the key path crowd scores a very strong point: KeyPath is the only current type of the language that is designed to contain all needed information about its target.

with (Int) -> Foo you actually have a prototype of the case you are looking for. That's how I used it in my solution with Mirror. You literally have all the informations you need about the wanted case.

Terms of Service

Privacy Policy

Cookie Policy