CS 330 Lecture 21 – More Recursion and Pattern Matching
Dear students,
Today we solve more problems in Haskell together. Along the way we will discover some of the peculiarities of functional programming, like the lack of iteration, the supremacy of recursion, and the beauty of pattern matching:
Here are the problems we’ll solve:
- Last time we started to write a function to determine if a list contains an element. Our first attempt used simple conditionals and recursion:
contains :: [a] -> Bool contains needle haystack = if haystack == [] then False else if head haystack == needle then True else contains needle $ tail haystack
This is okay, but Haskell sports a really nice guard syntax that shows the decision structure that we often see in mathematics:contains needle haystack | head haystack == needle = True | otherwise = False
A little shorter, right? Well, we also have acase
statement in Haskell, which allows us to use pattern matching:contains needle haystack = case haystack of [] -> False (first : rest) -> first == needle || contains needle rest ] This doesn't seem better than the guards. However, Haskell allows us to rewrite this structure in a more digestable way: [code contains _ [] = False contains (first:rest) = first == needle || contains needle rest
Under the hood these seemingly multiple definitions ofcontains
get turned into a case statement. But this syntax lets us focus on one case at a time. In my opinion, it is the pinnacle of clarity. - Let’s write
sum
to sum up a list of numbers in a similar way:sum [] = 0 sum (first:rest) = first + sum rest
- Let’s write
head'
:head' [] = error "Can't decapitate the empty list!" head' (first:rest) = first
Notice that in the second case we don’t userest
at all. Haskell lets us acknowledge its irrelevance by blanking out the identifier name:head' (first:_) = first
This feature is not unique to Haskell. Check out this from Ruby:The array returned bydef foo [1, 2] end first, second = foo
foo
is destructured intofirst
andsecond
. Suppose we don’t needsecond
:If we didn’t explicitly expressfirst, _ = foo
_
, no destructuring would take place, andfirst
would hold[1, 2]
. C++ also allows us to disregard parameters simply by not naming them in the formal parameters:Why would you ever write a function to take in a certain number of parameters and then completely ignore one? That sounds ridiculous. Except when you get into the world of inheritance, where you don’t get to choose your parameters.void foo(int a, int) { return f(a); }
- Let’s write
tail'
:tail' [] = error "Can't detorso the empty list!" tail' (_:rest) = rest
- Let’s write
belows
, which gives us back all elements of a list less than some threshold:belows [] = [] belows threshold (first:rest) | first < threshold = first : belows rest | otherwise = belows rest
- Let’s write an enum-like structure:
data Color = Red | Green | Blue | Yellow | Orange
- Let’s write
eval
, which evaluates an expression, just like we saw on the midterm. First, we need to create a hierarchy:data Expr = ExprInt Integer | ExprNegate Expr | ExprMultiply Expr Expr | ExprAdd Expr Expr
Now we can defineeval
:eval :: Expr -> Integer eval (ExprInt n) = n eval (ExprNegate e) = -eval e eval (ExprMultiply e1 e2) = eval e1 * eval e2 eval (ExprAdd e1 e2) = eval e1 + eval e2
We need a crazy expression to test this on. How about(17 + 1) * -(0 + 2))
?eval (ExprMultiply (ExprAdd (ExprInt 17) (ExprInt(1))) (ExprNegate (ExprAdd (ExprInt 0) (ExprInt 2)))
- Let’s write
contains
again, but this time let’s operate on a binary search tree:data Tree a = Leaf a | Branch (Tree a) a (Tree a) has :: Ord a => a -> Tree a -> Bool has needle (Leaf value) = value == needle has needle left value right | target == needle = True | target < needle = has needle left | otherwise = has needle right
See you next time, when we discuss the various ways we can define functions in Haskell!
Sincerely,