The Challenge
Courtesy of Mike Ash, here’s the challenge. You have an enumeration:
enum Enum { case foo(Int) case bar(String) case qux(Int) }
And you have an array of them:
let items: [Enum] = [.foo(1), .bar("hi"), .foo(2)]
Return a filtered array containing only one case, for example foo
. The difficulty lies in that Swift does not seem to offer a ==
or ~=
operator that works on cases, ignoring associated values:
// does not work let filtered = items.filter({ $0 == .foo })
So what do you do?
Attempt 1
Here’s my first attempt. Super ugly but it gets the job done:
let filtered = items.filter({ switch $0 { case .foo: return true; default: return false } })
Evan Dekhayser prefers if-case:
let filtered = items.filter({ if case .foo = $0 { return true }; return false })
And you can of course use guard
as well:
let filteredy = items.filter({ guard case .foo = $0 else { return false }; return true })
Attempt 2
Just as ugly but slightly shorter in terms of number of characters. But it does more work than #1:
let filtered = items.filter({ for case .foo in [$0] { return true }; return false })
Again, yuck.
Attempt 3
I really hate this approach because you have to implement a separate property for each case. Double yuck:
extension Enum { var isFoo: Bool { switch self { case .foo: return true; default: return false } } } let filtered = items.filter({ $0.isFoo })
Attempt 4
This is gross because it requires a placeholder value for the rhs, even though that value is never used. And no, you can’t pass an underscore here:
extension Enum { static func ~= (lhs: Enum, rhs: Enum) -> Bool { let lhsCase = Array(Mirror(reflecting: lhs).children) let rhsCase = Array(Mirror(reflecting: rhs).children) return lhsCase[0].0 == rhsCase[0].0 } } let filtered = items.filter({ $0 ~= .foo(0) })
Attempt 5
Then I got the idea into my head that you could use reflection. If you don’t supply a value to an enumeration case with an associated value, it returns a function along the lines of (T) -> Enum
. Here is as far as I got before I realized the enumeration *name* was not preserved in its reflection:
import Foundation extension Enum { var caseName: String { return "\(Array(Mirror(reflecting: self).children)[0].0!)" } static func ~= <T>(lhs: Enum, rhs: (T) -> Enum) -> Bool { let lhsCase = lhs.caseName let prefixString = "Mirror for (\(T.self)) -> " let typeOffset = prefixString.characters.count let typeString = "\(Mirror(reflecting: rhs).description)" let rhsCase = typeString.substring(from: typeString.index(typeString.startIndex, offsetBy: typeOffset)) return true } }
Yeah. Really bad, plus it doesn’t work.
Call for solutions
Since I didn’t really get very far with this, I’m throwing this out there as an open challenge. Can you come up with a parsimonious, readable, and less horrible (I was going to say “more elegant”, but c’mon) way to approach this? I suspect my first attempt may be the best one, which would make me sad.
12 Comments
How about this?
Its pretty much the same as the first attempt
Yes, but slightly lighter because it doesn’t have a
switch
.Do they still need to be wrapped in the enumeration? If not, you could `flatMap` out the payloads:
In my opinion, having them still wrapped in the enumeration is stupid. It’s like filtering an array of optionals with `.filter { $0 != nil }`.
By the way, no need for `.some(x)`, since non-optionals are automatically promoted to optionals.
If you’re filtering down to only a certain case, I think would want to extract the payload of that case. Anyway, here’s my effort (it’s horrible and hacky):
Two more “Solution”, works but still horrible, share for fun ; )
It would be nice if Swift would allow
case Enum.foo = $0
as boolean expression, making the following possible:
let filtered = items.filter({ case Enum.foo = $0 })
I agree that attemp 1 is the best approach to solve the challenge. But I see an interesting behavious using the dump() function.
public enum Show
{
case netflix(String)
case hbo(String)
}
let shows: [Show] = [ .netflix("The Ranch"), .netflix("Travelers"), .hbo("A Game of Thrones"), .netflix("OA"), .hbo("The Preacher") ]
shows.forEach({ dump($0) })
The snippet output is something like this:
▿ TempCode.Show.netflix
- netflix: "The Ranch"
▿ TempCode.Show.netflix
- netflix: "Travelers"
▿ TempCode.Show.hbo
- hbo: "A Game of Thrones"
▿ TempCode.Show.netflix
- netflix: "OA"
▿ TempCode.Show.hbo
- hbo: "The Preacher"
It seems that dump knows the type (Show) and his associated type (netflix or HBO). Maybe It’s a string based on Mirror output or maybe is some kind of Type prepared to support enum.
Take a look at dump implementation on the Apple’s Swift’s repository at GitHub? Maybe could be a good idea. 😉
How about that? Need to repeat Enum though 🙁
enum Enum: ComparableEnum {
case foo(Int)
case bar(String)
case qux(Int, String)
}
protocol ComparableEnum { }
extension ComparableEnum {
static func ~= (lhs: Self, builder: @escaping (A) -> Self) -> Bool {
guard let lhsDecomposition = Mirror(reflecting: lhs).children.first,
let lhsValue = lhsDecomposition.value as? A,
let rhsDecomposition = Mirror(reflecting: builder(lhsValue)).children.first,
lhsDecomposition.label == rhsDecomposition.label
else { return false }
return true
}
}
let items: [Enum] = [.foo(1), .bar("hi"), .qux(1, "hi"), .foo(2), .qux(2, "bye")]
let foos = items.filter { $0 ~= Enum.foo }
let quxs = items.filter { $0 ~= Enum.qux }
foos
quxs
I managed to create a solution that lets you write this:
let filtered = items.filter { $0 ~= Enum.foo }
Check it out:
https://gist.github.com/bradhilton/c58ec6c2d1a68b6de3261322e994aab0