We've reached chapter 7: More Functional Patterns. This is a pretty big chapter that covers a lot of ground, so to make it more digestible to you (and more manageable to me), I'm going to break this chapter up into multiple pieces. In this post, we'll be looking at pattern matching and case expressions. Later posts will cover higher-order functions, function composition, and pointfree style, so there's lots to look forward to!
Pattern matching
"Pattern matching is an integral and ubiquitous feature of Haskell". Thus opens the sub-chapter on pattern matching. If you've seen any amount of Haskell code, you've probably come across it, but in case you haven't (and if you have: just to make sure we're on the same page), here's a primer.
Pattern matching is a way for us to match values against certain 'patterns'. Depending on the context, a pattern can be a wide range of things, including specific strings, numeric literals and list syntax; pattern matching can match on any and all data constructors. Pattern matching can even let us match on the inner structure of the thing we're matching on, such as a list or a tuple. Let's have some examples.
We can check whether a provided Integer
is of a specific value, like in the following case:
isTheAnswer :: Integer -> Bool
isTheAnswer 42 = True
isTheAnswer _ = False
We can check whether a list contains zero, one, or many elements:
listState :: [a] -> String
listState [] = "The list is empty"
listState [_] = "The list has one element"
listState (_:_:_) = "The list has at least two elements"
We can also check which data constructor has been used to create a value and extract the constructor parameters from it or even check them for specific values:
data User = LoggedIn String | Anonymous
userInfo :: User -> String
userInfo Anonymous = "The user is anonymous"
userInfo (LoggedIn "admin") = "The user is an administrator"
userInfo (LoggedIn username) = "The user is " ++ username
This covers some of the patterns you might see used with pattern matching. Keep in mind that the order of the patterns matter: they are evaluated from top to bottom, and once a pattern matches, no more cases will get checked.
Covering all the cases
When pattern matching, you should always handle all cases to avoid partial functions. This might seem like a hassle, but you don't need to handle all the cases explicitly. As seen in the first example (isTheAnswer
), the _
pattern works as a catch-all, meaning that if no other patterns have matched yet, this will match anything.
In addition to just being vigilant about matching all possible combinations, you can also turn up the compiler's crankiness by using the -Wall
flag. This will give you warnings if your patterns are non-exhaustive.
Case expressions (or: more pattern matching)
Similarly to the pattern matching above, we also have case
expressions in Haskell. They work the same way as basic pattern matching, but the syntax is a bit different. In short, it gives you all the power that you get from pattern matching, but with a bit more syntax. Let's rewrite the listState
function above using a case expression to see the similarities:
listState :: [a] -> String
listState xs =
case xs of
[] -> "The list is empty"
[_] -> "The list has one element"
(_:_:_) -> "The list has at least two elements"
This looks very much like pattern matching (and serves the same purpose in this case), except that we use the case ... of
structure to match on a named variable instead of directly on an input. Which is more appropriate depends on your use case and preferences.
And that's it for today, kids. A shorter, more digestible format that covers only the most essential. I'll cover the rest of the chapter in this way, and then we'll see what works best moving forward. Next time: higher-order functions! Until then: take care!