Haskell Pattern Matching
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
ackermann m n), we should first take a look at the type
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.
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
(x:xs) syntax just splits the list into its
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