Pattern matching in Swift

Pattern matching in Swift

[[156836]]

One of Swift's great features is the extension of pattern matching. A pattern is a rule value used for matching, such as the case of a switch statement, the catch clause of a do statement, and the conditions of if, while, guard, and for-in statements.

For example, suppose you want to determine whether an integer is greater than, less than, or equal to zero. You could use an if-else if-else statement, although it's not very pretty:

  1. let x = 10  
  2. if x > 0 {
  3. print( "greater than zero" )
  4. } else   if x < 0 {
  5. print( "less than zero" )
  6. } else {
  7. print( "equals zero" )
  8. }

Using a switch statement would be much better. My ideal code would be this:

  1. // pseudocode  
  2. switch x {
  3. case > 0 :
  4. print( "greater than zero" )
  5. case < 0 :
  6. print( "less than zero" )
  7. case   0 :
  8. print( "equals zero" )
  9. }

But pattern matching doesn't support inequalities by default. Let's see if we can change that. To make things clearer, I'll ignore the >0 case and replace it with greaterThan(0), and I'll define this operator later.

Extended pattern matching

Swift's pattern matching is based on the ~= operator, which matches if the ~= value of the expression returns true. The standard library comes with four ~= operator overloads: one for Equatable, one for Optional, one for Range, and one for Interval. None of these meet our needs, although Range and Interval are very close, and you can read this article about them.

So we have to implement our own ~=. The prototype of this method is:

  1. func ~=(pattern: ???, value: ???) -> Bool

We know that this method must return a Bool, and that's exactly what we need, we need to know if the value matches the pattern. The next thing to ask ourselves is: what is the type of the parameter?

For the value, we can use Int, which is what we needed in the previous example. But let's generalize it to accept any type. In our case, the pattern is like greaterThan(001.png) or lessThan(001.png). More generally, the pattern should be a method, a method that takes a value as an argument and returns true or false. The value is of type T, so the pattern should be of type T -> Bool:

  1. func ~=(pattern: T -> Bool, value: T) -> Bool {
  2. return pattern(value)
  3. }

Now we need to define the methods greaterThan and lessThan to create the pattern. Be careful not to confuse the 0 in the pattern greaterThan(0) with the value we want to match. The parameter to greaterThan is part of the pattern, which will be used in the second step. For example, greaterThan(0) ~= x is the same as greaterThan(0)(x).

We know that the method greaterThan(0) must return a method that takes a value and returns Bool. So greaterThan must be a method that takes another value and returns the previous method. We restrict the parameter to Comparable in order to use Swift's > and < operators in the implementation:

  1. func greaterThan(a: T) -> (T -> Bool) {
  2. return { (b: T) -> Bool in b > a }
  3. }

This method accepts one parameter, calls a method that accepts more than one parameter and returns it. Methods like this are called Curried functions. (Some instance methods in Swift are Curried functions.) Swift provides a special syntax for Curried functions, just as their name suggests. Using this syntax, our method becomes like this:

  1. func greaterThan(a: T)(_ b: T) -> Bool {
  2. return b > a
  3. }
  4. func lessThan(a: T)(_ b: T) -> Bool {
  5. return b < a
  6. }

So we have the first version of the switch statement:

  1. switch x {
  2. case greaterThan( 0 ):
  3. print( "greater than zero" )
  4. case lessThan( 0 ):
  5. print( "less than zero" )
  6. case   0 :
  7. print( "equals zero" )
  8. default :
  9. fatalError( "will not happen" )
  10. }

Very nice, but look at the default, this solution doesn't give the compiler any hint to do sanity checking, so we have to provide a default. If you are sure that the pattern covers every possible value, it is a good idea to call fatalError() in the default, which indicates that this code will never be executed.

Custom Operators

Think back to the initial idea, and the pseudocode. Ideally, we would like to replace greaterThan(0) and lessThan(0) with >0 and <0.

Custom operators are controversial because other readers are often unfamiliar with them and they reduce readability. Back to our example, something like greaterThan(0) is perfectly readable, so it could be argued that a custom operator is not needed. But at the same time, everyone knows what >0 means. So let's try it, but as we'll see, it's not going to be pretty.

Our custom operators are unary - they have only one operand. Also, they are prefix operators (as opposed to postfix, where the operator comes after the operand). There can't be a space between the unary operator and the operand, because Swift uses spaces to distinguish between unary and binary operators. Also, < is not allowed as a prefix operator, so we have to use something else instead. (> is allowed as a prefix, but not as a postfix).

I suggest we use ~> and ~<. While ~> is not ideal because it only looks like an arrow, the tilde hints at the pattern matching operator ~=. Other operators I can think of (like >> and <<) are confusing.

Update 25 Sep: I learned from Nate Cook that the operator ~> already exists in the standard library. Although its implementation is not public, Nate discovered that it is used to increase the index of a collection. Given this, it may not be a good idea to use the same operator for a completely different purpose. You can choose a different one.

The actual implementation is not important. All we have to do is declare the operator and implement the methods, which are just delegates to the methods we already have, greaterThan and lessThan:

  1. prefix operator ~> { }
  2. prefix operator ~< { }
  3. prefix func ~>(a: T)(_ b: T) -> Bool {
  4. return greaterThan(a)(b)
  5. }
  6. prefix func ~ Bool {
  7. return lessThan(a)(b)
  8. }

Thus, our switch statement becomes:

  1. switch x {
  2. case ~> 0 :
  3. print( "greater than zero" )
  4. case ~< 0 :
  5. print( "less than zero" )
  6. case   0 :
  7. print( "equals zero" )
  8. default :
  9. fatalError( "will not happen" )
  10. }

Again, there is no space between operators and operands.

This is our limit, very close to the original plan, but obviously not perfect.

Update 9/19: Joseph Lord reminded me that Swift has a similar syntax:

  1. switch x {
  2. case _ where x > 0 :
  3. print( "greater than zero" )
  4. case _ where x < 0 :
  5. print( "less than zero" )
  6. case   0 :
  7. print( "equals zero" )
  8. default :
  9. fatalError( "will not happen" )
  10. }

This syntax, although it may not be as concise as our custom solution, is definitely good enough because you shouldn't create a custom syntax for such a simple purpose. However, our solution is general and can be applied in different places. Read on.

Other applications

By the way, the solution presented here is very general. Our overloaded pattern matching operator ~= works with any type T and any method taking a T that returns Bool. In other words, our implementation makes pattern ~= value as useful as pattern(value). Furthermore, switch value { case pattern: ... } works as useful as if pattern(value) { ... }.

Checking digital parity

Let's take a look at a few examples. First, a simple example illustrates its applicability, although it has little practical significance. Suppose you have a method isEven that checks whether a number is an even number:

  1. func isEven(a: T) -> Bool {
  2. return a % 2 == 0  
  3. }

Now:

  1. switch isEven(x) {
  2. case   true : print( "even number" )
  3. case   false : print( "odd number" )
  4. }

Can become:

  1. switch x {
  2. case isEven: print( "even number" )
  3. default : print( "odd number" )
  4. }

Note that by default, the following code is invalid:

  1. switch x {
  2. case isEven: print( "even number" )
  3. case isOdd: print( "odd number" )
  4. }
  5. // error: Switch must be exhaustive, consider adding a default clause  

Matching Strings

For a more practical example, let's say you want to match a string against a prefix or suffix. Let's start by writing two methods, hasPrefix and hasSuffix, that take two strings and check if the first argument is a prefix/suffix of the second. These are just variations of the existing standard library methods String.hasPrefix and String.hasSuffix, but with the arguments in a convenient order (prefix/suffix first, full string second). If you use Partially Applied Functions a lot and pass them to other methods, you'll find that you often need to repeat arguments to fit the parameters of the called method. Annoying, but not hard.

  1. func hasPrefix(prefix: String)(value: String) -> Bool {
  2. return value.hasPrefix(prefix)
  3. }
  4. func hasSuffix(suffix: String)(value: String) -> Bool {
  5. return value.hasSuffix(suffix)
  6. }

Now we can do this, which in my opinion is quite easy to read:

  1. let str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  
  2. switch str {
  3. case hasPrefix( "B" ), hasPrefix( "C" ):
  4. print( "starts with B or C" )
  5. case hasPrefix( "D" ):
  6. print( "starts with D" )
  7. case hasSuffix( "Z" ):
  8. print( "ends with Z" )
  9. default :
  10. print( "Other cases" )
  11. }

in conclusion

To solve our original problem, we came up with a general solution that can solve a lot of different problems. I find this to be true all the time, when you pass methods around as values, it can be used in places you wouldn't normally think of. This is one of the core concepts behind the claim that functional programming improves composability.

Extending Swift’s pattern matching system with new capabilities, both for built-in types and custom types, is extremely powerful. As always, be careful not to extend it too much. Even if a custom syntax looks cleaner than a more conservative solution, it can make the code harder to read for those who are not familiar with it.

<<:  Weekly crooked review: Let me give you tenderness with my chopped-off hands

>>:  Some "pitfalls" and "solutions" for iOS game development and submission

Recommend

Is it a rumor that spinach is high in iron?

We have heard countless rumors and lies since we ...

Nokia Lumia 930 WP8.1 review: excellent software but lackluster hardware

At the Build conference yesterday, Microsoft rele...

Foreign media: iPhone and iPad will use USB-C interface from next year

[[329485]] According to foreign media reports, Ap...

How to achieve effective user retention? Share 6 tips!

When it comes to product growth, one thing that m...

Many giants have tested Hongmeng system: 60% faster than Android

As we all know, Huawei has been suppressed and hi...

Heavy snowfall! Temperature drop! Avoid these roads during peak return hours →

Today (February 4) It is the seventh day of the S...