A Beautifully Elegant way to Set-Match

Challenge: By default, Swift’s switch statement uses equality matching when testing Set instances (for example). So how can you switch that up to use containment instead of equality? For example, say you want to test a set of insets presented as Set<Inset>? Here’s the standard Swift solution for containment:

switch insets {
case let sides where sides.isSuperset(of: [.top, .bottom]) : ...
case let sides where sides.isSuperset(of: .top) : ...
case let sides where sides.isSuperset(of: .bottom) : ...
}

It’s not awful, but couldn’t there be a more beautiful way to allow pattern matching against a set using a more Swifty statement that didn’t abuse its where clause?

Solution: Olivier Halligon came up with the following approach. He built a custom Set containment struct whose pattern matching operator (~=) is customized to apply superset detection:

public struct SetContainmentMatcher<T: Hashable> {
    public let set: Set<T>
    
    public static func ~=(
        lhs: SetContainmentMatcher<T>, 
        rhs: Set<T>) -> Bool {
        return rhs.isSuperset(of: lhs.set)
    }
}

public func containing<T>(_ set: Set<T>) -> SetContainmentMatcher<T> {
    return SetContainmentMatcher(set: set)
}

To use, build your switch using the now-global containing function. The function enables you to test your set against other sets using standard switch pattern matching.

public enum Inset { case top, bottom, left, right }

let sides: Set<Inset> = [.top, .right]
switch sides {
case containing([.top, .bottom]):
    print("contains: top, bottom")
case containing([.top]):
    print("contains: top") // matches here
case containing([.bottom]):
    print("contains: bottom")
default:
    print("default case")
}

Nice, isn’t it? Make sure to test for the largest sets first. If you invert the logic here, testing [.top] before [.top, .bottom], you may end up with unexpected behavior.

More good reading here on Olivier’s blog.

3 Comments

  • Happy to be the guy that asked the question which resulted in such a great solution! 🙂

    I dreamed of a switch that looked like this:

    switch sides {
    case contains [.top, .bottom] :
    // ..
    case contains .top :
    // ..
    }

    and between Olivier and “another well known guy” 😉 they came up with a solution that didn’t require a change to Swift evolution (yikes) and still left my code looking pretty.

  • Welcome Back! I hope you are feeling better!

  • I once built a sequence matching library in my free time to test my knowledge of generics and protocols: https://github.com/ctxppc/PatternKit/

Join the Discussion

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>