7
votes

I have a custom struct with a few fields, and I'd like to pattern match against it in a swift switch statement so I can customise the matching, by comparing one of the fields against a regex.

E.g. Given this structure:

struct MyStruct {
    let header: String
    let text: String
}

I'd like to pattern match like this:

switch(someInstance) {
    case ("h1", "[a-z]+"): ...
    case ("h1", "0-9+"): ...
}

I tried to get this to work using a pattern matching function as follows:

func ~=(pattern: (String, String), value: MyStruct) -> Bool {
    return value.header == pattern.0 && value.text.range(of: pattern.1, options: .regularExpression) != nil
}

But then Xcode (9) fails to compile with this error:

Tuple pattern cannot match values of the non-tuple type 'MyStruct'

The best I've been able to achieve is the following:

struct MatchMyStruct {
    let header: String
    let regex: String

    init(_ header: NSString, _ regex: String) {
        self.header = header
        self.regex = regex
    }
}

func ~=(pattern: MatchMyStruct, value: MyStruct) -> Bool {
    return value.header == pattern.header && value.text.range(of: pattern.regex, options: .regularExpression) != nil
}

This lets me pattern match like this:

switch(someInstance) {
    case MatchMyStruct("h1", "[a-z]+"): ...
    case MatchMyStruct("h1", "0-9+"): ...
}

While this is functional, I'd much prefer not to have to have the MatchMyStruct wrappers explicit like that.

It seems like swift has some magic secret sauce for pattern matching with tuples that's getting in the way. Is there anything I can do here?

2
Would switch((someInstance.header, someInstance.text)) work?vacawama
@vacawama that's a cool idea. I tried it but unfortunately it didn't work and still had the 'Tuple pattern cannot match...' errorOrion Edwards

2 Answers

3
votes

You could make a computed property to return a tuple:

struct MyStruct {
    let header: String
    let text: String

    var tuple: (String, String) { return (header, text) }
}

And then you can switch based upon tuple computed property:

switch(someInstance.tuple) {
case ("h1", "[a-z]+"):
    ...
case ("h1", "0-9+"):
    ...
default:
    ...
}

Or, if your intent was to perform regular expression matching:

switch(someInstance.tuple) {
case ("h1", let string) where string.range(of: "^[a-z]+$", options: .regularExpression) != nil:
    print("alphabetic")
case ("h1", let string) where string.range(of: "^[0-9]+$", options: .regularExpression) != nil:
    print("numeric")
default:
    print("other")
}

Or, if that's too much of a mouthful, you can define some string functions for regex pattern matching, e.g.:

extension String {
    func isMatch(regex pattern: String) -> Bool {
        return range(of: "^" + pattern + "$", options: .regularExpression) != nil
    }
    func contains(regex pattern: String) -> Bool {
        return range(of: pattern, options: .regularExpression) != nil
    }
}

And then:

switch(someInstance.tuple) {
case ("h1", let string) where string.isMatch(regex: "[a-z]+"):
    print("alphabetic")
case ("h1", let string) where string.isMatch(regex: "[0-9]+"):
    print("numeric")
default:
    print("other")
}

Or do this anyway you want, but it's just to illustrate that if you want tuple matching, you can just define computed property to return the tuple and then do whatever you want in the where clauses.

0
votes

Doesn't address the tuple-matching problem, but you can turn the pattern into a String array and still enjoy the expressivity:

func ~=(pattern: [String], value: MyStruct ) -> Bool {
    return pattern.count == 2 && (value.header as String) == pattern[0] && value.text.range(of: pattern[1], options: .regularExpression) != nil
}

switch someInstance {
    case ["h1", "[a-z]+"]: ...
    case ["h1", "[0-9]+"]: ...
    default: ...
}