Tail Recursion and Streams
Monday, 8/3
Announcements
Tail Recursion and Streams
Today is about making modifications to our interpreter
Also, how to do extra credit on the project
Number of call frames: Python
def factorial(n):
if n == 0:
return 1
else:
return n*factorial(n-1)
Global
factorial
func factorial(x)
f1: factorial
n
2
return value
2
f1: factorial
n
1
return value
1
f1: factorial
n
0
return value
1
O(n) frames
What if n is 1000? 2000? 10,000?
Maximum recursion depth exceeded
Number of call frames: Python
def factorial(n):
total = 1
while n > 0:
n, total = n - 1, n * total
return total
Global
factorial
func factorial(x)
f1: factorial
n
2
return value
2
total
1
1
2
0
2
O(1) frames
What if n is 1000? 2000? 10,000?
No problem!
Number of call frames: Scheme
def factorial(n):
total = 1
while n > 0:
n, total = n - 1, n * total
return total
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)))))
def factorial(n):
if n == 0:
return 1
else:
return n*factorial(n-1)
Python
Scheme
Python
Scheme
What kind of language doesn't have a while loop?!?!?!?!?!
An aside: Converting iteration to recursion (review)
(define (factorial n)
(define (helper n total)
(if (= n 0)
total
(helper (- n 1) (* n total))))
(helper n 1)
)
def factorial(n):
total = 1
while n > 0:
n, total = n - 1, n * total
return total
Stopping condition
Stopping condition
Updated variables
Updated variables
Updated values
Updated values
Initial values
Initial values
Tail Recursion and Streams
Tail recursion
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)) ) ))
(define (helper n total)
(if (= n 0)
total
(helper (- n 1) (* n total) ) ))
Tail recursion: a function whose recursive call is a tail call
Tail call: a function call that is the last expression evaluated in a function (i.e. in a tail context)
Tail recursive: last evaluated expression is the recursive call
Not tail recursive: the last function call is *, not the recursive call
Number of call frames: Regular recursion
Global
factorial
func factorial(x)
f1: factorial
n
2
return value
2
f1: factorial
n
1
return value
1
f1: factorial
n
0
return value
1
O(n) frames
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)))))
Number of call frames: Tail recursion
(define (helper n total)
(if (= n 0)
total
(helper (- n 1)
(* n total))))
(helper 2 1)
Global
helper
func helper(x)
f1: helper
n
2
return value
total
1
O(1) frames
f2: helper
n
1
return value
total
2
f3: helper
n
0
return value
2
total
2
Tail call optimization
Tail recursion: Is it iteration or recursion?
(define (helper n total)
(if (= n 0)
total
(helper (- n 1) (* n total))))
Tail Recursion and Streams
Example: Length of a list
(define (length lst)
'YOUR-CODE-HERE
)
Example: Length of a list
def length(lst):
total = 0
while lst is not Link.empty:
total = total + 1
lst = lst.rest
return total
Example: Length of a list
(define (length lst)
(define (helper total lst)
)
)
def length(lst):
total = 0
while lst is not Link.empty:
total = total + 1
lst = lst.rest
return total
Example: Length of a list
(define (length lst)
(define (helper total lst)
(if (null? lst)
total
)
)
)
def length(lst):
total = 0
while lst is not Link.empty:
total = total + 1
lst = lst.rest
return total
Example: Length of a list
(define (length lst)
(define (helper total lst)
(if (null? lst)
total
(helper (+ total 1) (cdr lst))
)
)
)
def length(lst):
total = 0
while lst is not Link.empty:
total = total + 1
lst = lst.rest
return total
Example: Length of a list
(define (length lst)
(define (helper total lst)
(if (null? lst)
total
(helper (+ total 1) (cdr lst))
)
)
(helper 0 lst)
)
def length(lst):
total = 0
while lst is not Link.empty:
total = total + 1
lst = lst.rest
return total
Tail Recursion and Streams
Naive scheme_eval
def scheme_eval(exp, env):
...
return scheme_apply(procedure, args, env)
def scheme_apply(procedure, args, env):
...
new_env = make_call_frame(procedure, env)
return eval_all(procedure.body, new_env)
f1: factorial
(define (helper n total)
(if (= n 0)
total
(factorial (- n 1) (* total n))))
scheme_eval
scheme_apply
scheme_eval
scheme_apply
f2: factorial
scheme_eval
...
Implementing tail call optimization
def scheme_optimized_eval(exp, env, tail):
if tail:
# Return Evaluating. Don't call scheme_apply
else:
result = Evaluating(exp, env)
while exp isinstance(result, Evaluating):
...
result = scheme_apply(procedure, args, env)
env = result.env
def scheme_apply(procedure, args, env):
...
new_env = make_call_frame(procedure, env)
return eval_all(procedure.body, new_env)
f1: factorial
scheme_eval
scheme_apply
scheme_eval
f2: factorial
f2: factorial
Tail Recursion and Streams
Lazy evaluation
scm> (ints 1)
maximum recursion depth exceeded
def ints(first):
while True:
yield first
first += 1
(define (ints first)
(cons first (ints (+ first 1)))
>>> s = ints(1)
>>> next(s)
1
>>> next(s)
2
Second argument to cons is always evaluated
Can represent large or infinite sequences
Streams
Instead of iterators,
Scheme uses streams
(define (ints first)
(cons first
(ints (+ first 1)))
scm> (ints 1)
maximum recursion depth exceeded
(define (ints first)
(cons-stream first
(ints (+ first 1)))
scm> (ints 1)
(1 . #[promise (not forced)])
Lazy evaluation, just like iterators in Python
Streams
scm> (define s (cons-stream 1 (cons-stream 2 nil)))
s
scm> s
(1 . #[promise (not forced)])
scm> (car s)
1
scm>
I don't need the rest of this list right now. Can you compute it for me later?
Sure, I promise to compute it right after I finish watching House of Cards.
Streams
scm> (define s (cons-stream 1 (cons-stream 2 nil)))
s
scm> s
(1 . #[promise (not forced)])
scm> (cdr s)
#[promise (not forced)]
scm>
I want the cdr of the list now.
Just one more episode. I'm almost done with season 2.
Streams
scm> (define s (cons-stream 1 (cons-stream 2 nil)))
s
scm> s
(1 . #[promise (not forced)])
scm> (stream-cdr s)
(2 . #[promise (not forced)])
scm> (stream-cdr (stream-cdr s))
()
scm>
stream-cdr !
Fine! Here's your stupid list.
Streams
scm> (stream-cdr s)
(2 . #[promise (not forced)])
scm> (define s (cons-stream 1 (cons-stream 2 nil)))
s
scm> (cdr s)
#[promise (not forced)]
Remember, a stream is just a regular Scheme pair whose second element is a promise
scm> (stream-cdr (stream-cdr s))
()
1
Promise:
(cons-stream 2 nil)
Promise:
(cons-stream 2 nil)
2
Promise:
nil
Promise:
nil
Tail Recursion and Streams
Promises: delay
scm> (print 5)
5
x
scm> (delay (print 5))
#[promise (not forced)]
(print 5) is not evaluated yet
(print 5) is immediately evaluated
Promise:
(print 5)
Promises: force
scm> (define x (delay (print 5)))
x
scm> x
#[promise (not forced)]
scm> (force x)
5
okay
scm> x
#[promise (forced)]
Promise:
(print 5)
Promise:
(print 5)
okay
Evaluates (print 5)
(print 5) is not evaluated yet
Promises
scm> (define s (cons 1 (delay nil))))
s
scm> s
(1 . #[promise (not forced)])
scm> (define s (cons-stream 1 nil)))
s
scm> s
(1 . #[promise (not forced)])
scm> (stream-cdr s)
()
scm> (force (cdr s))
()
1
Promise:
nil
Promise:
nil
cons-stream and stream-cdr are syntactic sugar. Achieve the same effect with delay and force
Tail Recursion and Streams
Examples: map-stream
(define (map-stream fn s)
'YOUR-CODE-HERE
)
Examples: map-stream
(define (map-list fn s)
(if (null? s)
nil
(cons (fn (car s))
(map-list fn (cdr s)))))
(define (map-stream fn s)
(if (null? s)
nil
(cons-stream (fn (car s))
(map-list fn (stream-cdr s)))))
What happens if you change this to cons?
Examples: stream-to-list
(define (stream-to-list s num-elements)
'YOUR-CODE-HERE
)
scm> (stream-to-list (ints 1) 10)
(1 2 3 4 5 6 7 8 9 10)
Examples: stream-to-list
(define (stream-to-list s num-elements)
(if (or (null? s) (= num-elements 0))
nil
)
Examples: stream-to-list
(define (stream-to-list s num-elements)
(if (or (null? s) (= num-elements 0))
nil
)
Examples: stream-to-list
(define (stream-to-list s num-elements)
(if (or (null? s) (= num-elements 0))
nil
(cons (car s)
(stream-to-list (stream-cdr s)
(- num-elements 1))))
)
Tail Recursion and Streams
Implementing streams and promises
class Promise:
...
def do_delay_form(exp, env):
...
def do_cons_stream_form(exp, env):
...
>>> str( Promise( Pair('+', Pair(4, Pair(3, nil))) , env) )
#[promise (not forced)]
>>> do_delay_form( Pair(Pair('+', Pair(4, Pair(3, nil))), nil) , env)
Promise(Pair('+', Pair(4, Pair(3, nil))), env)
>>> do_cons_stream_form( Pair(1, Pair(2, nil)) , env)
Pair(1, Promise(Pair(2, nil), env))