Lecture 08 Recursive Functions

Joseph Haugh

University of New Mexico

Review

What does this code produce?

[ x ^ 2 | x <- [1..5] ]

Review

What does this code produce?

[ x ^ 2 | x <- [1..5] ]

[ 1, 4, 9, 16, 25 ]

Review

What does this code produce?

[ (x, y, z) | x <- [1..3],
              y <- [x..3],
              z <- [y..3] ]

Review

What does this code produce?

[ (x, y, z) | x <- [1..3],
              y <- [x..3],
              z <- [y..3] ]

[ (1, 1, 1), (1, 1, 2), (1, 1, 3),
  (1, 2, 2), (1, 2, 3), (1, 3, 3),
  (2, 2, 2), (2, 2, 3), (2, 3, 3),
  (3, 3, 3) ]

Review

What does this code produce?

xs = zip [1..5] [ 1, 4, 9, 16, 25 ]
[ snd x | x <- xs, even (fst x) ]

Review

What does this code produce?

xs = zip [1..5] [ 1, 4, 9, 16, 25 ]
[ snd x | x <- xs, even (fst x) ]

[ 4, 16 ]

Objectives

  1. Understand the basic mechanics of recursion
  2. Start to gain an intuition for recursion
  3. Understand how to write recursive functions over numbers
  4. Understand how to write recursive functions over lists
  5. Define and use the steps to write a recursive function

Recursion

So far we have only defined functions in term of other functions

For example we could define the factorial function as follows:

factorial :: Int -> Int
factorial n = product [1..n]

Recursion

However, what if we wanted to directly define factorial?

Let’s first recall the mathematical definition of factorial:

0! = 1

n! = n * (n-1) * (n-2) * ... * 1
n! = n * (n-1)!

Recursion

0! = 1

n! = n * (n-1) * (n-2) * ... * 1
n! = n * (n-1)!

We can relatively easily translate that into Haskell:

factorial :: Int -> Int           -- The type is the same
factorial 0 = 1                   -- Base case
factorial n = n * factorial (n-1) -- Recursive case

Recursion

The first step to understanding recursion is to understand it mechanically, by which I mean the steps it takes during evaluation

Recursion

Let’s see what factorial 3 evaluates to step by step:

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n-1)
factorial 3
| {applying factorial}
3 * factorial 2
| {applying factorial}
3 * (2 * factorial 1)
| {applying factorial}
3 * (2 * (1 * factorial 0))
| {applying factorial}
3 * (2 * (1 * 1))
| {applying *}
6

Recursion Mistake

What if we had defined factorial like this?

factorial :: Int -> Int
factorial 0 = 0
factorial n = n * factorial (n-1)

Recursion Mistake

Let’s see what factorial 3 evaluates to with this new definition:

factorial :: Int -> Int
factorial 0 = 0
factorial n = n * factorial (n-1)
factorial 3
| {applying factorial}
3 * factorial 2
| {applying factorial}
3 * (2 * factorial 1)
| {applying factorial}
3 * (2 * (1 * factorial 0))
| {applying factorial}
3 * (2 * (1 * 0))
| {applying *}
0

We get 0 no matter what number we pass in!

Recursion Mistake: Identity Element

Our mistake was using the zero element instead of the identity element of multiplication.

A zero element is an element z such that for any element x:

x * z = z

Whereas an identity element is an element e such that for any element x:

x * e = x

Defining product

We previously defined factorial using product:

factorial n = product [1..n]

But how do we define product?

product' :: Num a => [a] -> a
product' []     = 1 -- Identity element of *
product' (x:xs) = x * product' xs

Defining product

Let’s see what product' [2,3,4] looks like:

product' :: Num a => [a] -> a
product' []     = 1
product' (x:xs) = x * product' xs
product' [2,3,4]
| {applying product'}
2 * product' [3,4]
| {applying product'}
2 * (3 * product' [4])
| {applying product'}
2 * (3 * (4 * product' []))
| {applying product'}
2 * (3 * (4 * 1))
| {applying *}
24

Mechanical Understanding of Recursion

When first starting out with recursion it may help to write out the steps of evaluation of various arguments as we just did

Exercise: length

Define the length' function using recursion. Also show the steps of evaluation of length' [1,2,3].

Reminder: length' returns the number of elements in a list.

length' :: [a] -> Int

Exercise: length

length' :: [a] -> Int
length' []     = 0
length' (_:xs) = 1 + length' xs

Exercise: length

length' :: [a] -> Int
length' []     = 0
length' (_:xs) = 1 + length' xs

length' [1,2,3]
| {applying length'}
1 + length' [2,3]
| {applying length'}
1 + (1 + length' [3])
| {applying length'}
1 + (1 + (1 + length' []))
| {applying length'}
1 + (1 + (1 + 0))
| {applying +}
3

Reverse

The reverse function takes in a list and reverses it, for example:

> reverse "geralt"
"tlareg"
> reverse "hello"
"olleh"

How do we define reverse?

Step by Step Guide: Reverse

When defining a recursive function, it may help to follow these steps:

  • Step 1: Define the type of the function
    • For now let’s say reverse takes a [Char] and returns a [Char]
reverse' :: [Char] -> [Char]

Step by Step Guide: Reverse

  • Step 2: Enumerate the cases
    • We need a pattern for the empty list
    • We need a pattern for a non-empty list
reverse' []     = ...
reverse' (x:xs) = ...

Step by Step Guide: Reverse

  • Step 3: Define the simple cases
    • What is the reverse of an empty list?
reverse' []     = []
reverse' (x:xs) = ...

Step by Step Guide: Reverse

  • Step 4: Define the other cases
    • What is the reverse of a non-empty list?
reverse' []     = []
reverse' (x:xs) = reverse' xs ++ [x]

Step by Step Guide: Reverse

  • Step 5: Generalize and simplify
    • Notice that we do not depend on the type of the list elements, so we can generalize the type.
reverse' :: [Char] -> [Char]

Step by Step Guide: Reverse

  • Step 5: Generalize and simplify
    • Notice that we do not depend on the type of the list elements, so we can generalize the type.
reverse' :: [a] -> [a]

Step by Step Guide: Reverse

  • Step 6: Test the function
> reverse' []
[]
> reverse' [1,2,3]
[3,2,1]
> reverse' "hello"
"olleh"

By following these steps, you can at least have a starting point for defining a recursive function.

Let’s try it again.

Step by Step Guide: take

take takes an Int n and a list xs and returns the first n elements of the list.

  • Step 1: Define the type of the function
take' :: Int -> [a] -> [a]

Step by Step Guide: take

  • Step 2: Enumerate the cases
    • We need a case for when the list is empty and n is 0
    • We need a case for when the list is not empty and n is 0
    • We need a case for when the list is empty and n is not 0
    • We need a case for when the list is not empty and n is not 0
take' 0 []     = ...
take' 0 (x:xs) = ...
take' n []     = ...
take' n (x:xs) = ...

Step by Step Guide: take

  • Step 3: Define the simple cases
    • What happens when you take 0 elements from an empty list?
    • What happens when you take 0 elements from a non-empty list?
    • What happens when you take n elements from an empty list?
take' 0 []     = []
take' 0 (x:xs) = []
take' n []     = []
take' n (x:xs) = ...

Step by Step Guide: take

  • Step 4: Define the other cases
    • What happens when you take n elements from a non-empty list?
take' 0 []     = []
take' 0 (x:xs) = []
take' n []     = []
take' n (x:xs) = x : take' (n-1) xs

Step by Step Guide: take

  • Step 5: Generalize and simplify
    • Why specifically take an Int?
    • Why not take an Integral
    • Why have two cases for when n is 0?
take' :: Int -> [a] -> [a]
take' 0 []     = []
take' 0 (x:xs) = []
take' n []     = []
take' n (x:xs) = x : take' (n-1) xs

Step by Step Guide: take

  • Step 5: Generalize and simplify
    • Why specifically take an Int?
    • Why not take an Integral
    • Why have two cases for when n is 0?
take' :: Integral b => b -> [a] -> [a]
take' 0 []     = []
take' 0 (x:xs) = []
take' n []     = []
take' n (x:xs) = x : take' (n-1) xs

Step by Step Guide: take

  • Step 5: Generalize and simplify
    • Why specifically take an Int?
    • Why not take an Integral
    • Why have two cases for when n is 0?
take' :: Integral b => b -> [a] -> [a]
take' 0 []     = []
take' 0 (x:xs) = []
take' n []     = []
take' n (x:xs) = x : take' (n-1) xs

Step by Step Guide: take

  • Step 5: Generalize and simplify
    • Why specifically take an Int?
    • Why not take an Integral
    • Why have two cases for when n is 0?
take' :: Integral b => b -> [a] -> [a]
take' 0 _      = []
take' n []     = []
take' n (x:xs) = x : take' (n-1) xs

Step by Step Guide: take

  • Step 6: Test the function
> take' 0 [1,2,3]
[]
> take' 100 []
[]
> take' 3 [1,2,3,4,5]
[1,2,3]
> take' 100 "hello"
"hello"

Summary

  1. Recursive functions are defined in terms of themselves
  2. We can define recursive functions over numbers and lists among other things
  3. Follow the 6 steps to help define recursive functions and gain intuition

Exercise: iota

Define a function iota which takes in an Int n and returns a list of the numbers from 1 to n.

iota :: Int -> [Int]
  • Step 1: Define the type of the function
  • Step 2: Enumerate the cases
  • Step 3: Define the simple cases
  • Step 4: Define the other cases
  • Step 5: Generalize and simplify
  • Step 6: Test the function

Exercise: iota

iota :: Int -> [Int]
iota 0 = []
iota n = iota (n-1) ++ [n]

Exercise: iota

iota :: Int -> [Int]
iota n = [1..n]

Exercise: iota

iota :: (Eq a, Num a) => a -> [a]
iota 0 = []
iota n = iota (n-1) ++ [n]

Exercise: iota mistake

Why is this wrong?

iota :: (Eq a, Num a) => a -> [a]
iota 0 = []
iota n = n : iota (n-1)

Let’s try to evaluate iota 3 step by step.

Exercise: iota mistake

iota :: (Eq a, Num a) => a -> [a]
iota 0 = []
iota n = n : iota (n-1)
iota 3
| {applying iota}
3 : iota 2
| {applying iota}
3 : (2 : iota 1)
| {applying iota}
3 : (2 : (1 : iota 0))
| {applying iota}
3 : (2 : (1 : []))
| {applying :}
[3,2,1]