Challenge: Filtering associated value enumeration arrays

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?

    let filtered = items.filter {
        if case .foo = $0 { return true }
        return false
    }
    let filtered = items.filter { thing in
        if case .foo = thing { return true }
        return false
    }
  • Its pretty much the same as the first attempt

    let filtered = items.filter { if case .foo = $0 { return true }; return false }
    • 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:

    items.flatMap { if case .foo(let x) = $0 { return .some(x) } else { return nil } }
    • 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.

  • enum Enum {
        case foo(Int)
        case bar(String)
        case qux(Int)
        
        static func == ( lhs: Enum, rhs: Enum )  -> Bool {
                switch (lhs, rhs) {
                case (.foo(_), .foo(_)):
                    return true
                case (.bar(_), .bar(_)):
                    return true
                case (.qux(_), .qux(_)):
                    return true
                default:
                    return false
                }
        }
    }
    
    let items: [Enum] = [.foo(1), .bar("hi"), .foo(2)]
    
    let filtered  = items.filter ({ $0 == .foo(0) })
  • 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):

    func match<A>(_ pattern: @escaping (A) -> B) -> (_ value: B) -> A? {
      return { value in
        for case let (lab?, val) in Mirror(reflecting: value).children {
          if let res = val as? A,
            let pat = Mirror(reflecting: pattern(res)).children.first?.label,
            lab == pat {
              return res
          }
        }
        return nil
      }
    }
    
    let items: [Enum] = [.foo(1), .bar("hi"), .foo(2)]
    
    let foos : [Int]    = items.flatMap(match(Enum.foo)) // [1, 2]
    let bars : [String] = items.flatMap(match(Enum.bar)) // ["hi"]
    let quxs : [Int]    = items.flatMap(match(Enum.qux)) // []
    
  • Two more “Solution”, works but still horrible, share for fun ; )

        enum Enum {
            case foo(Int)
            case bar(String)
            case qux(Int)
            
            var index: Int8 {
                var mutableSelf = self
                return withUnsafePointer(to: &mutableSelf) {
                    return $0.withMemoryRebound(to: (Int, Int8).self, capacity: 1) { $0.pointee }.1
                }
            }
            
            var prefix: String {
                return "\(self)".components(separatedBy: "(").first!
            }
            
            // 1.
            static func ~= (lhs: Enum, rhs: Enum) -> Bool {
                return lhs.index == rhs.index
            }
            
            // 2.
            static func ~= (lhs: Enum, rhs: Enum) -> Bool {
                return lhs.prefix == rhs.prefix
            }
        }
    
  • 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. 😉

  • 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