A big problem currently unresolved in the swift world is in regards to enums. Filtering and equating enums in swift is verbose and might require some extra helper function or computed var on the enum itself.
In HBCDigital I’m currently working with my team to a new reactive framework that will make easy to build an app having an events based architecture that aims to maximize modularization for features and various potentially reusable components: MERLin.
One of the core parts of this framework is the events layer. Our goal was to make easy the creation of modules and their events. Ultimately an Events producer should be able to send events. Events listeners should be able to listen to them, filter them, combine them…
While working to the events layer of this framework, we realized some interesting things about enums. I’m going to share in this article our findings.
The problem
Given an enum
enum Something { case simpleCase case complexCase(name: String) }
We can’t normally equate this enum, and we can’t make it conform to String or Int while some cases have a payload.
You can use a pattern matching like if let case .complextCase(name) = myEnumInstance { … } but unfortunately there is no direct way to extract the payload from the enum.
The “case” syntax is inapplicable outside of ifs and guards, so there is no concise syntax to filter and map array of enums.
You can overcome these problems by writing code in your enums. Adding a var that gives you their payload would help with map. Making them conform to equatable would help with filtering. In any case, you will have to write more code, making your enums smarter than they should be…
Another point of view
let complexCase = Something.complexCase(name: "Super complex!!!")
If it’s true that Something.complexCase(name: “Super complex!!!”) creates an instance of type Something, then Something.complexCase must be a function that takes in a String as a parameter (in this case) and spits out an instance of type Something. Generalizing this concept, an enum with payload can be initialised by using a function that looks like (Payload)->Type
That said, can we create a function that looks like this?
func payload<Type, Payload>(forCase this: Type, pattern: (Payload) -> Type) -> Payload?
This function would take as a parameter the case with the payload to extract, and a pattern to match the case. Ultimately, if there is a match, the function would return the payload of this, or nil
How to use it and why we need it?
let name = payload(forCase: myEnumInstance, pattern: Something.complexCase)
If we put it this way doesn’t look like we have any advantage in using this hypothetical function against the use of the if let case syntax. Let’s try to see which other advantages this function might give us.
Let’s suppose we have this kind of situation
enum Engineer { case senior(name: String, salary: Double) case midLevel(name: String, salary: Double) case junior(name: String, salary: Double) }
then we have an array of Engineers
let engineers: [Engineer] = [ .senior(name: "Dejan", salary: 1234), .senior(name: "Giuseppe", salary: 5678), .senior(name: "Luigi", salary: 9101), .junior(name: "Filippo", salary: 12), .midLevel(name: "Mario", salary: 34) ]
this array contains Seniors, midLevels and juniors.
let seniors = engineers.compactMap { payload(forCase: $0, pattern: Engineer.senior) } this one line will extract the payload filtering just seniors.
Still, nothing we could not do with a regular if let case, but what if we mix up things and we introduce protocols?
protocol Engineer { } struct Person { var name: String var salary: Double } //This might be an arguably wrong structure, but it helps to prove my point enum Senior: Engineer { case ios([Person]) case java([Person]) case scala([Person]) } enum Junior: Engineer { case ios([Person]) case java([Person]) case scala([Person]) } let engineers: [Engineer] = [ Senior.ios([ Person(name: "Dejan", salary: 1234), Person(name: "Giuseppe", salary: 1234) ]), Junior.ios([ Person(name: "Mario", salary: 1234), Person(name: "Giovanni", salary: 1234) ]), Senior.scala([ Person(name: "Joy", salary: 1234), Person(name: "John", salary: 1234) ]) ]
As you can see now our array of engineers is a bit more complex and a compactMap would have a higher complexity than the simple line let seniorIosEngineers = engineers.compactMap { payload(forCase: $0, pattern: Senior.ios) }
The function
func payload<Type, Payload>(forCase this: Type, pattern: (Payload) -> Type) -> Payload? { for case let (label?, value) in Mirror(reflecting: this).children { //1. if let result = value as? Payload, //2. let patternLabel = Mirror(reflecting: pattern(result)).children.first?.label, //3. label == patternLabel { //4. return result //5. } } return nil //6. }
- after mirroring this we get its children’s label and value.
- value is expected to be of type Payload
- if it’s true that value is of type Payload we can use the pattern function to create an instance of type Type. The result of pattern(result) will be exactly the case we are looking for. Mirroring this new instance we extract the label of its first child.
- The label is expected to be the same as the label for the child of this
- Finally, if the labels match we did find our result
- Otherwise, this does not match pattern, therefore we return nil
This function is using Mirroring. Interesting thing is that an enum case containing a payload, mirrors exactly like a struct having a variable where the label of the enum case is the same of the struct’s var, and the type is the actual payload (That can be a tuple)
enum ProductArrayEvent { case productsLoaded(products: [String]) } struct ProductArrayEventS { struct productsLoaded { var productsLoaded: [String] init(products: [String]) { productsLoaded = products } } } let loaded = ProductArrayEvent.productsLoaded(products: ["Something"]) let loadedStruct = ProductArrayEventS.productsLoaded(products: ["Something"]) print(Mirror(reflecting: loaded).children.map { "\($0) - \($1)" }) print(Mirror(reflecting: loadedStruct).children.map { "\($0) - \($1)" })
The two print here will print exactly the same thing.