Linear recursion #
Definition. A recursive function/method is linear recursive if it performs at most one recursive call each time it is executed.
Hint. In an imperative language (like Java), a linear recursive algorithm can easily be transformed into an iterative one (i.e. an algorithm that uses only loops).
Haskell does not admit loops, so linear recursion is the standard way to perform simple iterations (although list comprehensions can be used instead in certain cases). Accordingly, in this section, we will mostly use Haskell as an illustration.
Execution #
Example. The following function sums a list of integers:
1 2 3sum :: [Int] -> Int sum [] = 0 sum (x : xs) = x + sum xsLet us clarify how an expression like
sum [1,2,3]is evaluated.Fist, recall that
[1,2,3]is syntactic sugar for1 : (2 : (3 : [])).Therefore when the pattern
(x : xs)(Line 3) is matched against[1,2,3]:
xmatches1, andxsmatches2 : (3 : [])(or equivalently,[2,3])So the evaluation of
sum [1,2,3]performs a recursive callsum [2,3](Line 3).Besides, the
+operation is performed after the recursive call.So the following sequence of events takes place:
- the evaluation of
sum [1,2,3]starts- $\quad$ the evaluation of
sum [2,3]starts- $\quad\quad$ the evaluation of
sum [3]starts- $\quad\quad\quad$ the evaluation of
sum []starts- $\quad\quad\quad$ the evaluation of
sum []ends and returns0- $\quad\quad$ the evaluation of
sum [3]ends and returns3 + sum [](i.e.3)- $\quad$ the evaluation of
sum [2,3]ends and returns2 + sum [3](i.e.5)- the evaluation of
sum [1,2,3]end ands returns1 + sum [2,3](i.e.6)After Step 4, the call stack is:
sum [] sum [3] : 3 sum [2,3] : 3 sum [1,2,3] : 3 <rest of the stack>
Hint. Understanding the execution of a recursive program (as we did above) is mostly useful for debugging. In order to write a recursive program, one can very often use a simpler, high-level methodology, sketched immediately below.
Solving a problem recursively #
Hint. Recursion is often convenient to solve a problem (i.e. find an algorithm that performs a certain task), when the input is a collection or data structure: list, array, matrix, tree, graph, etc.
Methodology. To solve a problem recursively:
- (Base case). If the input $I$ is minimal (e.g. of size 0 or size 1), then solve the problem directly.
- (Inductive case). Otherwise:
- Within $I$, identify one or several candidate smaller input(s) $I_1$, .., $I_k$.
- Solve the problem for each $I_1$, .., $I_k$ by performing recursive calls.
- Use the results(s) of the recursive calls to solve the problem for $I$.
In other words, in the inductive case, solve the problem for $I$ under the assumption that it can be solved for $I_1$, .., $I_k$.
Example. Let us reproduce the function above:
sum :: [Int] -> Int -- base case (the input list has size 0) sum [] = 0 -- inductive case (the input list has size n + 1) sum (x : xs) = x + sum xsIn this example, the input $I$ is a list.
- In the base case ($I$ is the empty list), we solve the problem directly by returning
0.- In the inductive case, $I$ is of the form $(a \colon I_1)$, where $a$ is an integer and $I_1$ is a list. In this case, the sub-list $I_1$ is our (unique) smaller input. We:
- solve the problem for $I_1$ by calling the function
sumrecursively, and- use the result of
sum$I_1$ to solve the problem for the full list $I$, assuming that the recursive call is correct.
Hint. When the input $I$ is a list, the tail of $I$ is often a good candidate for a recursive call.
Correctness #
Correctness of the whole algorithm holds by induction on the size of the input, observing that:
- the base case is correct, and
- in the inductive case, if the recursive calls are correct, then function/method is correct.
Example. In the example above:
$a)$ The base case is clearly correct:
sum [] = 0$b)$ In the inductive case, if the recursive call
sum xsis correct, then the function is also clearly correct:sum (x : xs) = x + sum xsFrom these two observations $a)$ and $b)$, we can conclude that the function is correct for an input $I$ of any size:
$0)$ If $I$ has size $0$ (i.e. if it is empty), then from $a)$, the function is correct.
$1)$ If $I$ has size $1$, then the recursive call
sum xstakes as input a list of size $0$.
From $0)$, this recursive call is correct.
So from $b)$, the function is correct for $I$.$2)$ If $I$ has size $2$, then the recursive call
sum xstakes as input a list of size $1$.
From $1)$, this recursive call is correct.
So from $b)$, the function is correct for $I$.…
$n)$ …
$n+1)$ If $I$ has size $n +1$, then the recursive call
sum xstakes as input a list of size $n$.
From $n)$, this recursive call is correct.
So from $b)$, the function is correct for $I$.
Examples #
Reminder. Some of the exercises below ask you to reimplement functions that are already available in the Haskell Prelude. If you want to test your code, you need to give them different names (for instance
length'instead oflength).
Implement recursively the native function
length :: [a] -> Int
that maps a list to its length.
length :: [a] -> Int
length [] = 0
length (_ : xs) = 1 + length xs
Implement recursively the native function
reverse :: [a] -> [a]
that maps a list to the reversed list.
For instance,
reverse [2,3,1,3] should evaluates to [3,1,3,2].
reverse :: [a] -> [a]
reverse [] = []
reverse (x : xs) = reverse xs ++ [x]
Implement recursively the function
endsWith :: (Eq a) => a -> [a] -> Bool
such that
endsWith x xs is True iff xs ends with x.
endsWith :: (Eq a) => a -> [a] -> Bool
-- empty list
endsWith _ [] = False
-- list of size 1
endsWith x [y] = x == y
-- list of size > 1
endsWith x (_ : xs) = endsWith x xs
Implement recursively the native functions
map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
any :: (a -> Bool) -> [a] -> Bool
all :: (a -> Bool) -> [a] -> Bool
that we have seen earlier.
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x: xs) = f x : map f xs
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter f (x : xs)
| f x = x : filter f xs
| otherwise = filter f xs
any :: (a -> Bool) -> [a] -> Bool
any _ [] = False
any f (x : xs) = f x || any f xs
all :: (a -> Bool) -> [a] -> Bool
all _ [] = True
all f (x : xs) = f x && all f xs
Implement recursively the native function
zip :: [a] -> [b] -> [(a,b)]
such that zip [x1, x2, ...] [y1, y2, ...] evaluates to the list of pairs [(x1, y1), (x2, y2), ...].
The length of the output list is the length of the shorter of the two input lists.
For instance,
zip ['a','b','c'] [1,2]
should evaluate to [('a',1), ('b',2)].
zip :: [a] -> [b] -> [(a,b)]
zip _ [] = []
zip [] _ = []
zip (x : xs) (y : ys) = (x,y) : zip xs ys
Recall the function reverse :: [a] -> [a] seen earlier, which reverses a list.
Another useful function provided by the Haskell Prelude is takeWhile, defined as follows:
takeWhile :: (a -> Bool) -> [a] -> [a]
takeWhile _ [] = []
takeWhile f (x : xs) | f x = x : takeWhile f xs
| otherwise = []
Let us assume for simplicity that:
- words in a string are separated with a blank space (
' 'in Haskell), and - strings have no trailing
' '.
What does the function myFunction below do (recall that . stands for function composition)?
myFunction :: String -> String
myFunction = reverse . takeWhile (/= ' ') . reverse
It maps a string to its last word.