Spotlight: Rebecca Skinner (Author) Interview and AMA!

That’s a great question. There’s a lot of overlap, between the two, but there are some things you can do with a switch statement that you can’t do with pattern matching, and vice versa.

Let’s look at an example of where they overlap. One common way that people use switch statements is to pick a choice from a set of potential options. For example, if you were writing a program to display a list of files, you might want to match specific file extensions and show a more human readable name for the file type. In a language with switch statements you might say (in C-like pseudo-code):

string get_file_type_description(string file_name) {
string extension = get_file_extension(file_name);  
switch (extension) {
    case "png":
      return "Image Fiile";
    case "txt":
      return "Plain text file";
    default:
      return (extension + " file");

In this example, we’re matching each branch of the switch statement with an exact value, and returning a value based on that. In Haskell we can do the same thing with pattern matching. The usual way would be using a case expression, which looks pretty similar to switch:

getFileDescription :: FilePath -> String
getFileDescription fileName =
  case getFileExtension fileName of
    "png" -> "Image File"
    "txt"   -> "Plain text file"
    extension -> extension ++ " file"

The most notable difference in this example is that when we’re using pattern matching, we don’t have keywords like case and default. Instead, each branch starts with the pattern we want to match against (in this case, the exact string we want to match on). Instead of a default case, we just provide a variable that will end up holding whatever value is there if we didn’t match one of the other branches.

It’s a little surprising that the very common use case for switch statements and pattern matching with case overlap so much, because in a lot of other ways they offer really different features. One of the main selling points of pattern matching is that it allows you to quickly access data that has a fairly complicated “shape”. Let’s look at another common example of pattern matching with case: handling command line arguments.

In a larger application you might use a library to deal with command line arguments, but for short programs with only a couple of options, you might want to do it manually. Let’s say that you’re building a program to copy a file. You want to support a couple of different options for copying:

First, if the user passes in the --help flag, you want to display a help message. Otherwise, they should pass in a source file, a destination file, and an option number of bytes to copy We can use pattern matching to make the process a lot easier:

getProgramOptions :: [String] -> Either String ProgramOptions
getProgramOptions commandLineArgs =
  case commandLineArgs of
    [inputFile, outputFile] -> 
      makeProgramOptions inputFile outputFile CopyAllData
    [inputFile, outputFile, amount] ->
      makeProgramOptions inputFile outputFile (CopySomeData amount)
   _ -> showHelpText

In this example we’re using pattern matching to identify how many arguments were passed in, and to bind the value of those arguments to particular variables like inputFile and outputFile (the _ at the end says to match anything and ignore the values, like default). This is really convenient because it lets us combine the common concerns of “check how many arguments were passed in” and “assign the arguments to a variable” into a single expression.

Another benefit of pattern matching is that you can use it in more places than you would want to use an case expression. For example, you can commonly see pattern matching being used with functions. This function to recursively add up the numbers in a list is one example:

addNumbers [] = 0
addNumbers (thisNumber:rest) = thisNumber + addNumbers rest

In the first function definition, we match on the case where we have an empty list and return a value of 0. The second definition matches the first element of the list and the rest of the list into two parts. It’s safe, because we’ve already check for empty lists in the earlier version of the function.

Another case where pattern matching is useful is when you are defining values. For example, we can get each of the elements out of a tuple with pattern matching in a way that looks awfully similar to multi-return in languages like Go:

returnTwoNumbers = (5,7)
sumTwoNumbers =
  let (x,y) = returnTwoNumbers
  in x + y

Here, we’re using a pattern to make it easier for us to get at each of the values inside of the tuple. Without pattern matching we’d have to say:

sumTwoNumbers =
  let numbers = returnTwoNumbers
  in fst numbers + snd numbers

Of course, there are some situations where switch can do things that pattern matching doesn’t do. For example, some languages let you embed an arbitrary expression in your switch statements:

string file_size_units(int bytes) {
  switch(bytes)
    case (bytes > 1024):
      return "kb"
    case (bytes > 1024 * 1024)
      return "mb"
    case (bytes > 1024 * 1024 * 1024)
      return "gb"
    default:
      return "bytes"
}

There’s not a good way to use pattern matching for this in Haskell, but we can use a different feature, guard clauses to get the same ability:

fileSizeUnits bytes
  | bytes > 1024 = "kb"
  | bytes > 1024 * 1024 = "mb"
  | bytes > 1024 * 1024 * 1024 = "gb"
  | otherwise = "bytes"

Anyway, this response has gotten pretty long, but I hope it helps!

7 Likes