CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(interleave '(x y z) '(a b c)) = '(x a y b z c)
(interleave '(x y z) '(a)) = '(x a y z)
(interleave '(x) '(a b c)) = '(x a b c)
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
The elements after x are themselves an interleave, with the arguments swapped:
(interleave '(`a` `b` `c`) '(y z)) = '(`a` y `b` z `c`)
Thus:
(interleave '(x y z) '(`a` `b` `c`))
= (cons x (interleave (`a` `b` `c`) (list y z)))
= (cons x '(`a` y `b` z `c`))
= '(x `a` y `b` z `c`)
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
value = number | void | func-dec
func-dec = (lambda ( variable* ) term+ )
(define (r:value? v)
(or (r:number? v)
(r:void? v)
(r:lambda? v)))
(struct r:void () #:transparent)
(struct r:number (value) #:transparent)
(struct r:lambda (params body) #:transparent)
10(void)(lambda () 10)CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
value = number | void | func-dec
func-dec = (lambda ( variable* ) term+ )
(define (r:value? v)
(or (r:number? v)
(r:void? v)
(r:lambda? v)))
(struct r:void () #:transparent)
(struct r:number (value) #:transparent)
(struct r:lambda (params body) #:transparent)
10(void)(lambda () 10)(r:number 10) ; <- 1
(r:void) ; <- 2
(r:lambda (list) ; <- 3
(list (r:number 10)))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
expression = value | variable | apply
apply = ( expression+ )
(define (r:expression? e)
(or (r:value? e)
(r:variable? e)
(r:apply? e)))
(struct r:variable (name) #:transparent)
(struct r:apply (func args) #:transparent)
x(f 10)CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
expression = value | variable | apply
apply = ( expression+ )
(define (r:expression? e)
(or (r:value? e)
(r:variable? e)
(r:apply? e)))
(struct r:variable (name) #:transparent)
(struct r:apply (func args) #:transparent)
x(f 10); 1:
(r:variable 'x)
; 2:
(r:apply
(r:variable 'f)
(list (r:number 10)))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
term = define | expression
define =
( define identifier expression )
| ( define ( variable+ ) term+)
(define (r:term? t)
(or (r:define? t)
(r:expression? t)))
(struct r:define (var body) #:transparent)
(r:define (r:variable 'f)
(r:lambda (list (r:variable 'y))
(list
(r:apply (r:variable '+)
(list (r:variable 'y) (r:number 10))))))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
term = define | expression
define =
( define identifier expression )
| ( define ( variable+ ) term+)
(define (r:term? t)
(or (r:define? t)
(r:expression? t)))
(struct r:define (var body) #:transparent)
(r:define (r:variable 'f)
(r:lambda (list (r:variable 'y))
(list
(r:apply (r:variable '+)
(list (r:variable 'y) (r:number 10))))))
(define (f y) (+ y 10))
(define f
(lambda (y) (+ y 10)))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
In make-define-basic, given a quoted definition in node
x using parse-astparse-astr:defineCS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
In make-apply, given a quoted function call in node
parse-astr:applyCS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
In make-lambda, given a quoted lambda in node
r:lambdaCS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
In make-define-func, given a quoted function definition in node
parse-astparse-ast: go from list of symbols to list of variablesparse-ast: go from list of quoted terms to list of termsr:define that holds an r:lambdaCS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define (max l)
(match l
[(list) (error "max: expecting a non-empty list!")]
[(list x) x] ; The list only has one element (the max)
[(cons h l) #:when (> h (max l)) h] ; The max of the rest is smaller than 1st
[(cons _ l) (max l)])) ; Otherwise, use the max of the rest
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
We use a local variable to cache a duplicate computation.
(define (max2 x y) (if (> x y) x y))
(define (max l)
(match l
[(list) (error "max: expecting a non-empty list!")]
[(list h) h]
[(cons h l)
* (define rest-max (max l)) ; Cache the max of the rest
* (max2 h rest-max)]))
45,000$\times$ more elements for the same amount of time!
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define (max l)
* ; 1. Abstract the maximum between two numbers
(define (max2 x y) (cond [(< x y) y] [else x]))
* ; 2. Use parameters to store accumulated results
(define (max-aux curr-max l)
* ; 3. Accumulate maximum number before recursion
(match l
[(list) curr-max]
[(cons h l)
(define new-max (max2 curr-max h))
(max-aux new-max l)])) ; Otherwise, recurse
(match l
[(list) (error "max: empty list")] `; 4. Only test if the list is empty once`
[(cons h l) (max-aux h l)]))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
| Element count | Execution time | Increase | |
|---|---|---|---|
| Attempt #2 | 1,000,000 | 101.15ms | |
| Attempt #3 | 1,000,000 | 20.98ms | 4.8$\times$ speedup |
| Attempt #2 | 10,000,000 | 1410.06ms | |
| Attempt #3 | 10,000,000 | 237.66ms | 5.9$\times$ speedup |
Why is attempt #3 so much faster?
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define rest-max (max l))
(max2 h rest-max)]))
(define new-max (max2 curr-max h))
(max-aux new-max l)
(max2 h (`max` l))
(max-aux (`max2` curr-max h) l)
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
Call Stack: To be able to call and return from functions, a program internally maintains a stack called the call-stack, each of which holds the execution state at the point of call.
Activation Frame: An activation frame maintains the execution state of a running function. That is, the activation frame represents the local state of a function, it holds the state of each variable.
Push: When calling a function, the caller creates an activation frame that is used by the called function (e.g., to pass arguments to the function being called).
Pop: Before a function returns, it pops the call stack, freeing its local state.
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define (fact n)
(cond
[(= n 1) 1]
[else
(* n (fact (- n 1)))]))
(fact 3)
(* 3 (fact 2))
(* 3 (* 2 (fact 1)))
(* 3 (* 2 1))
(* 3 2)
6
[n=3,return=(* 3 (fact 2))]
[n=3,return=(* 3 ?)],[n=2,return=(* 2 (fact 1))]
[n=3,return=(* 3 ?)],[n=2,return=(* 2 ?)],[n=1,return=1]
[n=3,return=(* 3 ?)],[n=2,return=2]
[n=3,return=6]
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
Recursive functions pose a problem to this execution model, as the call-stack may grow unbounded! Thus, most non-functional programming languages are conservative on growing the call stack.
def fact(n):
return 1 if n <= 1 else n * fact(n - 1)
fact(1000)
File "<stdin>", line 1, in fact
RecursionError: maximum recursion depth exceeded
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define (fact n)
(define (fact-iter n acc)
(cond
[(= n 1) acc]
[else
(fact-iter (- n 1) (* acc n)) ]))
(fact-iter n 1))
(fact 3)
(fact 3)
(fact-iter 3 1)
(fact-iter 2 3)
(fact-iter 1 6)
6
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
[n=3,return=(fact-iter 3 1)]
[n=3,return=?],[n=3,acc=1,return=(fact-iter 2 3)]
[n=3,return=?],[n=3,acc=1,return=?],[n=2,acc=3,return=(fact-iter 1 6)]
[n=3,return=?],[n=3,acc=1,return=?],[n=2,acc=3,return=?],[n=1,acc=6,return=`6`]
[n=3,return=?],[n=3,acc=1,return=?],[n=2,acc=3,return=`6`]
[n=3,return=?],[n=3,acc=1,return=`6`]
[n=3,return=`6`]
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
The tail position of a sequence of expressions is the last expression of that sequence.
(lambda ()
exp1
; ...
`expn`) <-- tail position
When a function call is in the tail position we name it the tail call.
(lambda ()
exp1
; ...
`(f ...`)) <-- f is a tail call
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
A tail call does not need to push a new activation frame! Instead, the called
function can "reuse" the frame of the current function. For instance, in (fact 3), the call (fact-iter 3 1) is a tail call.
[n=3,return=(fact-iter 3 1)]
[n=3,return=?],[n=3,acc=1,return=(fact-iter 2 3)]
Can be rewritten with:
[n=3,return=(fact-iter 3 1)]
[n=3,acc=1,return=(fact-iter 2 3)]
In attempt #2, both calls to fact-iter are tail calls.
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
map, using the generalized tail-recursion optimization pattern(define (map f l)
(define (map-iter accum l)
(match l
[(list) (accum (list))]
[(cons h l) (map-iter (lambda (x) (accum (cons (f h) x))) l)]))
(map-iter (lambda (x) x) l))
The accumulator delays the application of (cons (f h) ?).
(lambda (x) x), which simply returns whatever list is passed to it.x,
and return (cons (f h) x) to the next accumulator.The accumulator works like a pipeline: each inductive step adds a new stage to the pipeline, and the base case runs the pipeline:
((lambda (x) x) (stage1 (stage2 (stage3 nil))))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
map run(map f (list 1 2 3)) =
; First, build the pipeline accumulator
(define (accum0 x) x) (map-iter accum0 (list 1 2 3)) =
(define (accum1 x) (accum0 (cons (f 1) x))) (map-iter accum1 (list 2 3)) =
(define (accum2 x) (accum1 (cons (f 2) x))) (map-iter accum2 (list 3)) =
(define (accum3 x) (accum2 (cons (f 3) x))) (map-iter accum3 (list)) =
; Second, run the pipeline accumulator
(accum3 (list)) =
(accum2 (list (f 3))) =
(accum1 (list (f 2) (f 3))) =
(accum0 (list (f 1) (f 2) (f 3))) =
(list (f 1) (f 2) (f 3))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
foldrfoldrProcessing a list of size: 1000000
-------------------------------------------------------
Throughput (unopt): 7310 elems/ms
Mean (unopt): 136.8±7.56ms
-------------------------------------------------------
Throughput (tailrec): 12349 elems/ms
Mean (tailrec): 80.98±1.49ms
Speed-up (tailrec): 1.7
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define (foldr step base-case l)
(foldl step base-case (reverse l)))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
(define (foldr step base-case l)
(foldl step base-case (reverse l)))
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
Processing a list of size: 1000000
-------------------------------------------------------
Throughput (unopt): 7310 elems/ms
Mean (unopt): `136.8±7.56ms`
-------------------------------------------------------
Throughput (tailrec): 12349 elems/ms
Mean (tailrec): `80.98±1.49ms`
Speed-up (tailrec): 1.7
-------------------------------------------------------
Throughput (rev+foldl): 4846 elems/ms
Mean (rev+foldl): `206.34±3.33ms`
Speed-up (rev+foldl): 0.7
CS450 ☽ Building the AST / Tail recursion ☽ Lecture 09 ☽ Tiago Cogumbreiro
We learned to generalize two reduction patterns (foldl and foldr)
(define (reverse l)
(define (rev accum l)
(match l
[(list) accum]
[(cons h l) (rev (cons h accum) l)]))
(rev (list) l))
(define (reverse l)
(define (on-elem elem accum)
(cons elem accum))
(foldl on-elem (list) l))