H. Conrad Cunningham
11 September 2018
Copyright (C) 2017, 2018, H. Conrad Cunningham
Acknowledgements: I originally created these slides in Fall 2017 to accompany what is now Chapter 9, Recursion Styles and Efficiency, in the 2018 version of the textbook Exploring Languages with Interpreters and Functional Programming.
Browser Advisory: The HTML version of this document may require use of a browser that supports the display of MathML. A good choice as of September 2018 is a recent version of Firefox from Mozilla.
Introduce different styles of recursive programs
linear and nonlinear – backward and forward
tail recursion — logarithmic
Explore their effects on time and space efficiency
Examine how to consider termination, preconditions, and postconditions in function design
Introduce local definitions
Recursive function with at most one recursive application occurring in any leg of definition
Precondition? Postcondition?
Termination?
Time complexity? (e.g., count multiplications, recursive calls)
Space complexity? (e.g., count max active stack frames)
Recursive function in which the evaluation of some leg requires more than one recursive application
Precondition? Postcondition?
Termination?
Time complexity? Space complexity?
fib n combinatorially
explosive in time
Algorithms matter! Linear better than superlinear
Linear recursion can be optimized to loop, perhaps using separate stack
Nonlinear recursion more difficult to optimize
Recursive application embedded within another expression
Evaluation work to do after recursive call returns
Compiler can optimize backward linear to loop (using stack)
Backward recursion often easy to write, reason about
Can we convert to more efficient function?
Recursive application not embedded within another expression
Evaluation work done in argument list (before call if eager)
Add auxiliary function factIter whose second argument
accumulates result incrementally
    factIter :: Int -> Int -> Int  -- auxiliary function, two args
    factIter 0 r         = r
    factIter n r | n > 0 = factIter (n-1) (n*r)  -- forward recursivePrecondition of factIter n r?
Postcondition of factIter n r?
Termination of factIter n r?
Time complexity of factIter n r? for optimized?
Space complexity of factIter n r? for optimized?
Recursive application both forward and linear recursive — factIter tail recursive
Compiler can optimize as loop without separate stack, reuses runtime stack frame, returns directly to caller
factIter parameter r accumulating parameter
accumulator standard method for converting backward to tail
factIter more general
than fact4 – equivalent if
accumulator initially 1
    fib2 :: Int -> Int
    fib2 n | n >= 0 = fibIter n 0 1 
        where 
            fibIter 0 p q         = p   -- two accumulators
            fibIter m p q | m > 0 = fibIter (m-1) q (p+q)Precondition of fibIter n p q? Postcondition?
Termination of fibIter n p q?
Time complexity of fibIter n p q?
fib(n-1) + fib(n-2)
by cheap p + qSpace complexity of fibIter n p q? When
optimized?
Observation: for n >= 0, b^n =
Precondition? Postcondition?
Termination?
Time complexity? Space complexity?
    expt2 :: Integer -> Integer -> Integer
    expt2 b n | n < 0 = error ("expt2 undefined for negative exponent" 
                                   ++ show n )
    expt2 b n         = exptIter n 1
        where exptIter 0 p = p
              exptIter m p = exptIter (m-1) (b*p) -- tail recursivePrecondition of exptIter n p? Postcondition?
Termination of exptIter n p?
Time complexity of exptIter n p? Space
complexity?
Observation: for n >= 0, b^n =
    b^n = b^(n/2)^2   if n is even
    b^n = b * b^(n-1) if n is oddIncorporate in expt3
    expt3 :: Integer -> Integer -> Integer
    expt3 _ n | n < 0 = error ("expt3 undefined for negative exponent"
                               ++ show n )
    expt3 b n         = exptAux n
        where exptAux 0 = 1
              exptAux n
                | even n   = let exp = exptAux (n `div` 2) in -- local
                                 exp * exp       -- backward recursive
                | otherwise = b * exptAux (n-1)  -- backward recursive    expt3 :: Integer -> Integer -> Integer
    expt3 _ n | n < 0 = error ("expt3 undefined for negative exponent"
                               ++ show n )
    expt3 b n         = exptAux n
        where exptAux 0 = 1
               exptAux n
                   | even n    = let exp = exptAux (n `div` 2) in -- local
                                     exp * exp      -- backward recursive
                   | otherwise = b * exptAux (n-1)  -- backward recursivePrecondition of expt3 b n? Postcondition?
Termination of exptAux n?
Time complexity of exptAux n? Space complexity?
Add local definitions
let —
bottom-up — introduce, then use – nested definitions
where —
top-down — use, then define
let Definitions (1)
letlocal_definitionsinexpression
    f :: [Int] -> [Int]
    f [] = []
    f xs = let square a = a * a 
               one      = 1 
               (y:ys)   = xs 
           in (square y + one) : f ys square function with 1
parameter
one constant — function
with 0 parameters
(y:ys) pattern
match against argument xs of
f — list patterns later
let Definitions (2)    f :: [Int] -> [Int]
    f [] = []
    f xs = let square a = a * a 
               one      = 1 
               (y:ys)   = xs 
           in (square y + one) : f ys Reference to y or ys when argument xs of f nil gives error
square, one, y, ys all come into scope simultaneously
— expression following in
keyword
xs in definition of (y:ys) from
parameter – outer scope
Local definitions may be recursive and call each other
where Definitionswhere clause
more versatile than let
Can span over several guarded equations
    g :: Int -> Int
    g n | check3 == x = x
        | check3 == y = y
        | check3 == z = z * z
                      where check3 = n `mod` 3
                            x      = 0
                            y      = 1
                            z      = 2where scope
all three guards and right-hand sides
Note where begins
in same column as =
Local definition can introduce shared component
Shared component only reduced once in graph reduction
check3 shared among all
three legs — evaluated once for each call of g.
Styles of recursion (linear and nonlinear, backward and forward, tail, logarithmic)
Correctness (precondition, postcondition, and termination)
Efficiency estimation (time and space complexity)
Transformations to improve efficiency (auxiliary function, accumulator)
Local definitions