Haskell Pattern Matching


One of the most interesting aspects of computer science are programming languages, they are the means by which the user and the computer interact and in their choices support certain modes of thought. Some modes of thought are promoted more than others, the dominant style being imperative programming in which you tell the computer explicitly what to do and how to do it. Intuitively this makes a lot of sense, and for that reason it is by far the most dominate paradigm for programming. Besides being intuitive, it is also promoted much more that other options, beginners start with Python, Ruby or JavaScript and for that reason they perpetuate the same ideas, and the same notions of how you program.

This has the fortunate side effect of making programming more generally accessible, but at the cost of new schools of thought and the possibility of better ways of doing things. For the past few weeks I've been starting to learn more about Functional Programming in Haskell and it is difficult to understand, but there are loads of interesting a great ideas that other languages miss completely.

Take for example this implementation of the Ackermann program. It illustrates a few of the features that we're interested in and is also rather interesting due to how quickly the program builds in complexity.

ackermann :: Int -> Int -> Int
ackermann m n
    | m == 0 = n + 1
    | m > 0 && n == 0 = ackermann (m-1) 1
    | otherwise = ackermann (m-1) $ ackermann m (n-1)

This Ackermann function is a great introduction to the notion of pattern matching. Before we look at pattern matching or guards (the pipes after ackermann m n), we should first take a look at the type signature. ackermann is a function that takes two Ints and returns another Int. When looking at this many people who are more used to Python, JS or other high level languages would wonder why bother with these type signatures? What do they get us that high level dynamically typed languages don't have?

Well the first key benefit is a key component of what makes Haskell interesting, Haskell features a static type system, which in short means that your errors are caught at compile time as opposed to run time. If we ever had an instance in which ackermann would take some other datatype we would know before running the program and wondering where we went wrong. This is important for catching errors, but more importantly it changes how you think about your functions. Instead of diving into coding something you sit down and divide the functions into responsibilities and types. This makes programming a much more careful and thought out process.

The second interesting aspect of ackermann is pattern matching. If you look at line two in ackermann you see m and n. ackermann is called in the form: ackermann 2 3, which matches m to 2 and n to 3. You may say this is no different from other languages, but not so! We'll have to look at another example in order to get a better understanding of why pattern matching is useful.

intersperse :: t -> [[t]] -> [t]
intersperse _ []     = []
intersperse _ (x:[]) = x
intersperse sep (x:xs) = x ++ [sep] ++ intersperse sep xs

In a sense, the patterns here are doing what if statements would do in other languages. You can read the first pattern as follows: ignore the first variable, if the second is an empty list return an empty list. Simple right! The biggest reason for the patterns is ease of use when it comes to recursive function calls. If you look at the intersperse function defined above you can start to get a sense of the benefits. Guards which were shown in the ackermann function are closer to the if statements that you've probably seen before or case statements. You can read m == 0 = n + 1 as if m is equal to 0 then return n + 1, in the ackermann function this case is the base case, meaning the case that terminates the recursive calls. With the ackermann function this can take quite a while but you can see the same thing in a simple implementation of the map function.

map :: (a -> b) -> a -> b
map _ [] = []
map f (x:xs) = f x : map f xs

The (x:xs) syntax just splits the list into its head and tail the head is the first element in the list and the tail is everything else. You can see that the function f applies itself to the head of the list, then a new list is made when the map function call is made again.

This was a basic introduction to Haskell Pattern Matching, I hope you found it somewhat useful. To learn more take a look at Real World Haskell or Learn You a Haskell For Great Good.