teaching machines

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:

  1. 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 a case 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 of contains 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.
  2. Let’s write sum to sum up a list of numbers in a similar way:
    sum [] = 0
    sum (first:rest) = first + sum rest
  3. 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 use rest 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:
    def foo
      [1, 2]
    end
    first, second = foo
    
    The array returned by foo is destructured into first and second. Suppose we don’t need second:
    first, _ = foo
    
    If we didn’t explicitly express _, no destructuring would take place, and first would hold [1, 2]. C++ also allows us to disregard parameters simply by not naming them in the formal parameters:
    void foo(int a, int) {
      return f(a);
    }
    
    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.
  4. Let’s write tail':
    tail' [] = error "Can't detorso the empty list!"
    tail' (_:rest) = rest
  5. 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
  6. Let’s write an enum-like structure:
    data Color = Red | Green | Blue | Yellow | Orange
  7. 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 define eval:
    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)))
  8. 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,

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *