Swift Advanced Custom Pattern Matching

Mar 17, 2023#patterns#swift

When you use pattern matching in Swift, the compiler uses the ~= operator behind the scenes to match the value against each case pattern.

By default, the ~= operator compares two values of the same type using the == operator. It can also match a value with a range of values, by checking whether the value is contained within the range.

The Swift Standard Library provides default implementations of the ~= operator for several built-in types, such as Int, String, and Optional, but developers can also define their own implementations of the operator for custom types or custom pattern matching logic.

To overload the ~= operator in Swift, you need to define a function that takes two parameters: a value to match and a pattern to match against.

Here are some examples to demonstrate how this works:

Matching against an array pattern

func ~=<T>(pattern: [T], value: T) -> Bool {
    return pattern.contains(value)
}

let fruits = ["apple", "banana", "orange"]
let fruit = "banana"

if case fruit = fruits {
    print("The fruit is in the list")
} else {
    print("The fruit is not in the list")
}

In this example, we’ve defined an overload for the ~= operator that matches a value against an array pattern. The function takes an array pattern and a value, and returns true if the value is contained in the pattern. We then use this operator overload in a case statement to match a string fruit against an array of fruits, and print a message indicating whether the fruit is in the list or not.

Matching against a range pattern

func ~=<T: Comparable>(pattern: ClosedRange<T>, value: T) -> Bool {
    return pattern.contains(value)
}

let number = 5
if case 1...10 = number {
    print("The number is in the range")
} else {
    print("The number is outside the range")
}

In this example, The overload function takes a ClosedRange pattern and a value, and returns true if the value is contained within the pattern.

Matching against a regular expression pattern

import Foundation

func ~= (pattern: NSRegularExpression, value: String) -> Bool {
    return pattern.firstMatch(in: value, range: NSRange(value.startIndex..., in: value)) != nil
}

let email = "johndoe@example.com"

if let regex = try? NSRegularExpression(pattern: "[a-z]+@[a-z]+\\.[a-z]+") {
    if case regex = email {
        print("The email is valid")
    } else {
        print("The email is invalid")
    }
}

// Prints: The email is valid

In this example, the overload function takes an NSRegularExpression pattern and a String value, and returns true if the value matches the regular expression.

Matching against custom pattern

protocol MyPattern {
    associatedtype Input
    func matches(_ value: Input) -> Bool
}

func ~=<P: MyPattern>(pattern: P, value: P.Input) -> Bool {
    return pattern.matches(value)
}

struct MyIntPattern: MyPattern {
    typealias Input = Int
    let patternValue: Int

    func matches(_ value: Int) -> Bool {
        return value % patternValue == 0
    }

    init(_ patternValue: Int) {
        self.patternValue = patternValue
    }
}

let number = 10

if case MyIntPattern(5) = number {
    print("\(number) is a multiple of 5")
} else {
    print("\(number) is not a multiple of 5")
}

// Prints: 10 is a multiple of 5

In this example, we define a protocol MyPattern. Then we overload the ~= operator to work with any type that conforms to the MyPattern protocol. Finally, we define a concrete implementation of the MyPattern protocol with MyIntPattern. It takes an Int pattern value and checks whether a given value is a multiple of the pattern.