Extract Payload for enum cases having associated value

This is a long requested feature that never saw the light. We have memories of other proposals pitched with Automatically derive properties for enum cases and before with Pitch: Even Smarter KeyPaths? from @stephencelis

How to access an associated value of an enum case is a very recurring question on stackOverflow:



Also on this forum the question appeared with a specific and more than valid use case by @zoul :

With the arrival of Combine I feel it's the right moment to pitch the proposal again, offering a different approach than the already proposed synthesised vars and smarter KeyPaths.

I personally liked what was alternatively proposed in this answer from the previously mentioned pitch, I in fact used this approach in my reactive framework MERLin to define an Event as enum and extract its associated value (if any) in a stream of events.

This allows me to do something like

// Given an Event Type
enum HomeEvent {
    case loaded
    case selectedProduct(Product)
    case favoritedProduct(Product)
}

// And an Observable of events
let homeEvent: Observable<HomeEvent> { return _homeEvent.asObservable() }
let _homeEvent = PublishSubject<HomeEvent>()

...
func didSelect(item: Int) {
    let product = products[item]
    _homeEvent.onNext(.selectedProduct(product))
}
...

Observing the occurrences of an event would be as simple as

homeEvent
    .capture(event: HomeEvent.selectedProduct) // custom operator using `associatedValue(matching:)` 
    .subscribe(onNext: { product in //product is of type Product
        ...
    }.disposed(by: disposeBag) 
// While this is RxSwift, this use case applies to any reactive framework
// including Combine

In MERLin I use Mirror to extract the payload of any enum conforming EventProtocol.

I believe that similarly to CaseIterable, these functionalities should be accessible on conformance to a protocol.

CaseAccessible

public protocol CaseAccessible {
    var label: String { get }
    
    func associatedValue<AssociatedValue>(mathing pattern: (AssociatedValue) -> Self) -> AssociatedValue?
    
    mutating func update<AssociatedValue>(value: AssociatedValue, matching pattern: (AssociatedValue) -> Self)
}

Just like it happen for CaseIterable, a default implementation of these functions should be synthesised by the compiler for enums with at least one case having associatedValue.

It is currently possible to try this solution in a playground using Mirroring:

public extension CaseAccessible {
    var label: String {
        return Mirror(reflecting: self).children.first?.label ?? String(describing: self)
    }
        
    func associatedValue<AssociatedValue>(mathing pattern: (AssociatedValue) -> Self) -> AssociatedValue? {
        guard let decomposed: (String, AssociatedValue) = decompose(),
            let patternLabel = Mirror(reflecting: pattern(decomposed.1)).children.first?.label,
            decomposed.0 == patternLabel else { return nil }
        
        return decomposed.1
    }
    
    mutating func update<AssociatedValue>(value: AssociatedValue, matching pattern: (AssociatedValue) -> Self) {
        guard associatedValue(mathing: pattern) != nil else { return }
        self = pattern(value)
    }
    
    private func decompose<AssociatedValue>() -> (label: String, value: AssociatedValue)? {
        for case let (label?, value) in Mirror(reflecting: self).children {
            if let result = (value as? AssociatedValue) ?? (Mirror(reflecting: value).children.first?.value as? AssociatedValue) {
                return (label, result)
            }
        }
        return nil
    }
    
    subscript<AssociatedValue>(case pattern: (AssociatedValue) -> Self) -> AssociatedValue? {
        get {
            return associatedValue(mathing: pattern)
        } set {
            guard let value = newValue else { return }
            update(value: value, matching: pattern)
        }
    }

    subscript<AssociatedValue>(case pattern: (AssociatedValue) -> Self, default value: AssociatedValue) -> AssociatedValue {
        get {
            return associatedValue(mathing: pattern) ?? value
        } set {
            update(value: newValue, matching: pattern)
        }
    }

}

Overloading cases are currently correctly handled by the compiler that gives an error asking for more context, if needed.

enum Foo: CaseAccessible {
    case bar(int: Int)
    case bar(str: String)
}

let baz = Foo.bar(int: 10)
let value = baz.associatedValue(matching: Foo.bar) // ERROR: Ambiguous use of 'bar'
let value: String? = baz.associatedValue(matching: Foo.bar) // nil
let value: Int? = baz.associatedValue(matching: Foo.bar) // Optional(10)

// or with subscripts

let value: Int? = baz[case: Foo.bar] // Optional(10)
let value: Int = baz[case: Foo.bar, default: 0] // 10

The compiler presently works well also when the overloaded case has same type

enum Foo: CaseAccessible {
    case bar(int: Int)
    case bar(int2: Int)
}

let baz = Foo.bar(int: 10)
let value = baz.associatedValue(matching: Foo.bar) // ERROR: Ambiguous use of 'bar'
let value: String? = baz.associatedValue(matching: Foo.bar) // ERROR: Cannot invoke 'associatedValue' with an argument list of type '(matching: _)' (because there is no bar having a String as payload
let value = baz.associatedValue(matching: Foo.bar(int:)) // Optional(10)
let value = baz[case: Foo.bar(int:)] // Optional(10)
let value = baz[case: Foo.bar(int:), default: 0] // 10

Working with array or observables of enums then becomes really simple

enum Foo: CaseAccessible {
    case bar(String)
    case baz(String)
    case bla(Int)
}

let events: [Foo] = [
    .bar("David"),
    .baz("Freddy"),
    .bar("Bowie"),
    .baz("Mercury"),
    .bla(10)
]

Then to compactMap through associated values is as simple as

let davidBowie = events
    .compactMap { $0[case: Foo.bar] }
    .joined(separator: " ") //"David Bowie"

let freddyMercury = events
    .compactMap { $0[case: Foo.baz] }
    .joined(separator: " ") //"Freddy Mercury"

Today this is already possible with pattern matching, but readability is not the best

let davidBowie = events
    .compactMap { 
        guard case let .bar(value) = $0 else { return nil }
        return value
    }
    .joined(separator: " ") //"David Bowie"

let freddyMercury = events
    .compactMap { 
        guard case let .baz(value) = $0 else { return nil }
        return value
    }
    .joined(separator: " ") //"Freddy Mercury"

Mutability

CaseAccessible has also an update function that will update the associated value of an enum case, if the pattern is matching

enum State: CaseAccessible {
    case count(Int)
    case error(String)
}

var currentState: State

currentState.update(value: 10, case: State.count)
// or by using subscripts
currentState[case: State.count] = 10
currentState[case: State.count, default: 0] += 1

Extensions

CaseAccessible would open the way to new extensions for Collection, Observable (RxSwift), Signal (ReactiveSwift) and AnyPublisher (Combine) for filtering and mapping:

extension Collection where Element: CaseAccessible {
    func filter<AssociatedValue>(case pattern: (AssociatedValue) -> Element) -> [Element] {
        return filter {
            $0[case: pattern] != nil
        }
    }
    
    func compactMap<AssociatedValue>(case pattern: (AssociatedValue) -> Element) -> [AssociatedValue] {
        return compactMap { $0[case: pattern] }
    }
    
    func exclude<AssociatedValue>(case pattern: (AssociatedValue) -> Element) -> [Element] {
        return filter {
            $0[case: pattern] == nil
        }
    }
}

enum ProductDetailEvent: CaseAccessible {
    case loaded(Product)
    ...
}

let events: [ProductDetailEvent] = ...

let selectedEvent = events.filter(case: ProductDetailEvent.loaded) // [ProductDetailEvent]
let loadedProducts = events.compactMap(case: ProductDetailEvent.loaded) // [Product]

What does the swift community think?

3 Likes

Could you please provide a summary of exactly what you intend for the compiler to synthesize, preferably with examples showing how we can achieve the same thing manually?

2 Likes

You can copy paste in a Playground my EventProtocol example in the post, and make your enum conform EventProtocol

I repeat my question.

3 Likes

CaseIterable, Codable, PropertyWrapper: There is "compiler magic" behind these features for you not to write code and have certain behaviours for free, if you comply to certain constraints.

CaseIterable: enum with purely sum types cases, otherwise you'll have to write which are "allCases"
Codable: all the variables must be Codable, otherwise you'll have to conform the CodableProtocol yourself

For these functions I'm proposing, all enums having at least one case with associated values will have them.

You can copy paste in a Playground my EventProtocol example in the post, and make your enum conform EventProtocol

public protocol EventProtocol {}

public extension EventProtocol {
    var label: String {
        return Mirror(reflecting: self).children.first?.label ?? String(describing: self)
    }
    
    func extractPayload<Payload>() -> Payload? {
        return decompose()?.payload
    }
    
    func extractPayload<Payload>(ifMatches pattern: (Payload) -> Self) -> Payload? {
        guard let decomposed: (String, Payload) = decompose(),
            let patternLabel = Mirror(reflecting: pattern(decomposed.1)).children.first?.label,
            decomposed.0 == patternLabel else { return nil }
        
        return decomposed.1
    }
    
    private func decompose<Payload>() -> (label: String, payload: Payload)? {
        for case let (label?, value) in Mirror(reflecting: self).children {
            if let result = (value as? Payload) ?? (Mirror(reflecting: value).children.first?.value as? Payload) {
                return (label, result)
            }
        }
        return nil
    }
}

enum HomeEvent: EventProtocol {
    case loaded
    case selectedProduct(Product)
    case favoritedProduct(Product)
}

I hope I solved your doubts

I am asking you to provide an example, here in this thread where everyone can see it, that shows both what an enum looks like with all the functionality you want implemented manually, and what that same enum looks like with the feature you are proposing.

7 Likes

At least some cases can already be accomplished today via e.g.:

let davidBowie = events
  .compactMap { if case .bar(let name) = $0 { return name } else { return nil } }
  .joined(separator: " ") //"David Bowie"

This pitch proposes a slightly more terse syntax the applies to such cases as:

let davidBowie = events
  .compactMap { $0.extractPayload(ifMatches: Foo.bar) }
  .joined(separator: " ") //"David Bowie"

Fixing the inability of Swift to match cases concisely - because case matching cannot be used outside of a full if or switch statement - would seemingly make the former even shorter again, and be a much broader win for the language.

For that scenario.

The other that this pitch is considering is to allow for extracting associated values ‘blindly’ based on type alone. My first instinct is that this might not be a good thing, actually. Swift seems to have made a fairly deliberate choice w.r.t. how its enums work - you’re expected to work centric to the cases, and any associated values only make sense if taken in the context of their case. Otherwise, you might as well and logically probably should just have properties on the enum itself, e.g.:

enum Foo {
  case bar
  case baz
  case bla

  var stringValue: String?
  var intValue: Int?
}

Swift doesn’t support this today, of course, but maybe it’s a better alternative iff language changes are necessary?

Though, are they?… today you already can add computed properties to any enum - including in extensions - that will seemingly do what you want, e.g.:

enum Foo {
  case bar(String)
  case baz(String)
  case bla(Int)

  var artistName: String? {
    switch self {
      case .bar(let name):  return name
      case .baz(let name):  return name
      default:  return nil
    }
  }
}

Would that not be cleaner & sufficient, since not every enum wants this functionality? That reads much better when you want to fetch across multiple cases, e.g.:

let davidBowie = events
  .compactMap { $0.artistName }
  .joined(separator: " ")
2 Likes

I apologise. I misunderstood the first time you asked me this.

Here is an example:


enum Foo {
    case baz(str: String)
    case baz(int: Int)
    case bla(int: Int)
    
    var label: String {
        return Mirror(reflecting: self).children.first?.label ?? String(describing: self)
    }
    
    func extractPayload<Payload>() -> Payload? {
        return decompose()?.payload
    }
    
    func extractPayload<Payload>(ifMatches pattern: (Payload) -> Foo) -> Payload? {
        guard let decomposed: (String, Payload) = decompose(),
            let patternLabel = Mirror(reflecting: pattern(decomposed.1)).children.first?.label,
            decomposed.0 == patternLabel else { return nil }
        
        return decomposed.1
    }
    
    private func decompose<Payload>() -> (label: String, payload: Payload)? {
        for case let (label?, value) in Mirror(reflecting: self).children {
            if let result = (value as? Payload) ?? (Mirror(reflecting: value).children.first?.value as? Payload) {
                return (label, result)
            }
        }
        return nil
    }
}

let intBaz = Foo.baz(int: 10)
let strBaz = Foo.baz(str: "baz")
let bla = Foo.bla(int: 20)

// indistinct extractPayload

let intBazValue: Int? = intBaz.extractPayload() // Optional(10)
let strBazValue: String? = strBaz.extractPayload() // Optional("baz")
let blaValue: Int? = bla.extractPayload() // Optional(20)

let all = [
    intBaz,
    strBaz,
    bla
]

let allStr: [String] = all.compactMap { $0.extractPayload() } // ["baz"]
let allInt: [Int] = all.compactMap { $0.extractPayload() } // [10, 20]

// pattern matching extraction

let intBazValue2 = intBaz.extractPayload(ifMatches: Foo.baz(int:)) //Optional(10)
let strBazValue2 = intBaz.extractPayload(ifMatches: Foo.baz(str:)) //Optional("baz")
let blaValue2 = bla.extractPayload(ifMatches: Foo.bla)// Optional(20)

let allRepeated = [
    intBaz,
    intBaz,
    intBaz,
    strBaz,
    strBaz,
    strBaz,
    bla
]

let allStrBaz = allRepeated.compactMap { $0.extractPayload(ifMatches: Foo.baz(str:)) } // ["baz", "baz", "baz"]
let allIntBaz: [Int] = allRepeated.compactMap { $0.extractPayload(ifMatches: Foo.baz(int:)) } // [10, 10, 10]

There are use cases for this choice. Consider an enum in which each case associated value is representing the same thing.

enum ItemUnion {
    case featuredImageItem(Item)
    case featuredTextItem(Item)
    case newArrivalsItem(Item)
    case imageItem(Item)
    case productItem(Item)
    case textItem(Item)
    case designersItem(Item)
    case shopsItem(Item)
    case videoItem(Item)
    case textOnlyPromoItem(Item)
    case imageWithTitleItem(Item)
    case individualSaleItem(Item)
    case feedProductItem(Item)
}

In this use case, there are areas of the app that might be interested in knowing the exact case and cases that can deal with Any Item indistinctly, hence the unconditioned extractPayload

While we can argue that the union can have a computed var, or that the whole thing can be wrote in a different way, the point of this pitch is to support this feature natively without requiring to write computed vars ourselves, and without giving these computed vars automatically to who doesn't want them

There are 2 important things to solve here:

  1. Ergonomic access to associated values (ideally as ergonomic as struct field access).
  2. Key path-like support for enum payloads.

Key paths have unlocked a ton of possibilities in library code, and we've seen a proliferation of their use even in Apple's latest Combine and SwiftUI frameworks, but enums are completely left behind because they don't have such a mechanism.

4 Likes

I honestly don't believe that keypaths are the way to handle this. That's the reason this proposal is different than the previous ones. If you match a specific pattern you get a tuple that you know how to handle. Today is already possible to extract a payload from an enum case, and I proved it in my code examples in my initial post. I believe we should just make it official. If one day keypaths will handle tuples, from this proposal enums will benefit as well

1 Like

OK, well then I disagree with the pitch. This should quite obviously be written in a different way.

struct TaggedItem {
  enum Kind {
    case featuredImage
    case featuredText
    //...
  }

  var kind: Kind
  var item: Item
}

An enum where all the payloads have the same type is better expressed as a struct, simple as that. Associated values for enums are most useful when different cases have different types. That makes it difficult to have a magic "extract payload" function; it doesn't fit the type system because the cases all have the same type (which is... you know, kind of the point of the enum in the first place).

This seems to me like you are writing the thing in the most awkward way possible, then complaining that it's awkward.

10 Likes

I'll not get in specific implementation details. There are cases where it is useful to get the payload if there is a pattern matching and cases where the pattern is not as important as the type. The pitch try to address both use cases. You can write anything without enums. To discard the whole proposal just for one use case that you don't like maybe it's too much, don't you think?

I'm not complaining about anything. There are at least three more use cases I described before that you cannot address easily with structs.

extractPayload(ifMatches: ) is a more concise way to do

if case let .foo(payload) = yourCase { return payload }
else return nil

and the extractPayload() function is a better way to extract a payload than having to write a computed var in your enum to get the value of the payload in your enum cases

But a major part of enginering is to use the correct tool for the job. I don't feel that you're using enums properly in any of your examples.

Just blindly extracting a string from an enum payload, without knowing its case, is not a good way to model your problem. If you really want to treat them as independent pieces of data (which, again, is not really the proper use-case for enums), you should split them and make them truly independent. Note that you can still ensure tags and payloads have the correct corresponding types in your initialisers. For example:

struct Foo {
  enum Tag { case bar, baz }
  enum Payload { case string(String), integer(Int) }

  var tag: Tag
  var payload: Payload

  private init(_ tag: Tag, _ payload: Payload) { ... }
  public static func bar(_ payload: String) -> Foo { return Foo(.bar, .string(payload)) } 
  public static func baz(_ payload: Int) -> Foo    { return Foo(.baz, .integer(payload)) } 
}

With a structure like this, you can easily extract either the tag or payload and handle them independently.

This are not sum types tough.

In my example of the ItemUnion I recognised that it could be wrote in a different way, but in the other examples, I believe that enums are a better way to describe events. Your Foo struct has many combinations of tag - Payload than the enum with associated value, and it is not self documenting at all.

struct Foo {
  enum Tag { case bar, baz }
  enum Payload { case string(String), integer(Int) }

  var tag: Tag
  var payload: Payload

  private init(_ tag: Tag, _ payload: Payload) { ... }
  public static func bar(_ payload: String) -> Foo { return Foo(.bar, .string(payload)) } 
  public static func baz(_ payload: Int) -> Foo    { return Foo(.baz, .integer(payload)) } 
}

bar can have both String and Int, just like baz, while

enum Foo {
 case bar(Int)
 case baz(str: String)
 case baz(int: Int)
}

the combinations are limited to the ones defined in the enum. A switch can easily be exhaustive.
Without counting the easiness to add a new case in the enum versus the counterpart using struct (add two cases, plus the static function. Last, but not least, with the struct you'll have to switch over the payload to get the value. the whole code gets much more verbose.

You are reasoning on enums limitations that I'm trying to overcome with this proposal. If you get extractPayload they do become the right tool for the job.

The main use case I would like to see this pass is this one:

enum HomeEvent: EventProtocol {
    case loaded
    case selectedProduct(Product)
    case favoritedProduct(Product)
}

let homeEvent: Observable<HomeEvent> { return _homeEvent.asObservable() }
let _homeEvent = PublishSubject<HomeEvent>()

...
func didSelect(item: Int) {
    let product = products[item]
    _homeEvent.onNext(.selectedProduct(product))
}
...

homeEvent
    .capture(event: HomeEvent.selectedProduct) // custom defined using `extractPayload(ifMatches:)`
    .subscribe(onNext: { product in //product is of type Product
        ...
    }.disposed(by: disposeBag) 
// While this is RxSwift, this use case applies to any reactive framework
// including Combine

Key paths already handle tuples but your proposal would miss half of the story if tuples were returned: the "writable" mechanism of the key path would be completely missing. Key paths are compiler-generated code for handling getting and setting of sub-state on a value. It allows us to write powerful library code that can not only access data (as in Foundation's KVO), but manipulate it (as in the Combine and SwiftUI frameworks, and many third party libraries).

Enums are currently completely left out of the key path story, even though they can contain sub-state: their associated values. This means it's currently impossible to write library code providing hooks to manipulating enums in a generic way. I think as long as we're talking about enum data access, we need to also consider data manipulation. So I think it's very important to consider the larger picture and include the discussion of a key path-like mechanism for enums.

3 Likes

@Karl, Consider the following example

enum ProductDetailEvent {
    case loaded(Product)
    case selectedSku(String, forProduct: Product)
    case changedSize(Size)
    case changedColor(Color)
}

with your approach of using a struct would become

struct ProductDetailEvent {
    enum Tag { case loaded, selectedSku, changedSize, changedColor }
    enum Payload { case product(Product), skuAndProduct(String, Product), size(Size, color(Color) }
    
    var tag: Tag
    var payload: Payload
    
    private init(_ tag: Tag, _ payload: Payload) { ... }
    public static func loaded(_ payload: Product) { ... }
    public static func selectedSku(String, forProduct: Product) { ... }
    public static func changedSize(_ size: Size) { ... }
    public static func changedColor(_ color: Color) { ... }
}

First you see how much work I had to do just to define ProductDetailEvent.

let event: ProductDetailEvent = .selectedSku("1234", product: Product())

guard case . selectedSku = event.tag, 
    case let . skuAndProduct(sky, product) = event.payload else { ... }
...

To access the payload I must do even more job than having to handle the enum the standard way.
With this proposal

let event = ProductDetailEvent.selectedSku("1234", forProduct: Product())
let payload = event.extractPayload(ifMatches: ProductDetailEvent.selectedSku)

and you can move on.

You can use it stream of events, to filter an array, and even to write array extensions and in case of reactive frameworks to write new operators to filter events. (like I did with capture(event:))

From the point of view of readability, within those 5 lines of the enum it's clear which events ProductDetail can emit, and what to expect as payload in those events. From the struct, not really and the matching is manual (see the guard in the example). extractPayload exploits the compiler to give precise and typeSafe extraction, where the compiler will stop from extracting payloads that are not possible given your cases. Mistakes are blocked by the compiler, the struct approach will just be error prone.

// with Struct
let event: ProductDetailEvent = .selectedSku("1234", product: Product())

guard case .selectedSku = event.tag, 
    case let .product(product) = event.payload else { ... }
 //Will compile but never work. it's a bug

//with enums
let payload: Product? = event.extractPayload(ifMatches: ProductDetailEvent.selectedSku)
//Will just not compile, and the compiler will also tell you why

But this is not the point of the proposal. I don't want enums to be writeable. I just want to access the payload, and this is all this proposal is about. This works well with tuples too. If you check on MERLin, these functions are 100% unit tested. they work with tuples, with optionals, with closures as associated types and with overloading cases. It is not my intention to manipulate enum instances, and in fact I think it would even be wrong.

Can you elaborate why you think it is wrong? Enums and structs are two sides of the same coin. Can you imagine if we couldn't manipulate struct data?

3 Likes

Enums are not same as structs, or they would have no reasons to exist. You just use them for different purposes. If you need to alter their value, maybe you need a struct.

Usually you use enums to define a state, an action, an event. Associated values of an enum should be strictly tied with the logic that actually generated the instance of the enum. Making it mutable would open the use of enums to side effects and unpredictability that you usually don't want if you decided to use an enum in the first place.

If you need a mutable payload, you just don't need an enum. I would prefer to stay on topic tough. We could discuss about this, but I feel like this is not the place. The proposal of having enum payload accessible via KeyPaths got rejected and I would prefer to discuss an eventual re-post in that Pitch topic. Here I'm proposing just to access the payload, using a more concise and flexible way than pattern matching in if and guards.

Terms of Service

Privacy Policy

Cookie Policy