WARNING : You must move to the advanced language level in DrScheme starting with this lecture. Let's return to our find-flights program. This version is a bit cleaned up from the previous one in the notes. ;; find-flights : city city -> (listof city) or false ;; creates a path of flights from start to finish (define (find-flights start finish) (local [(define rm ...) (define (find-helper start finish visited) (cond [(symbol=? start finish) (list start)] [(memq start visited) false] [else (local [(define possible-route (find-flights/list (direct-cities start) finish (cons start visited)))] (cond [(boolean? possible-route) false] [else (cons start possible-route)]))])) ;; direct-cities : city route-map -> (listof city) ;; return list of all cities in route map with direct flights from ;; given city (define (direct-cities from-city) (local [(define from-city-info (filter (lambda (c) (symbol=? (city-info-name c) from-city)) rm))] (cond [(empty? from-city-info) empty] [else (city-info-fly-to (first from-city-info))]))) ;; find-flights/list : ;; (listof city) city (listof city) -> (listof city) or false ;; finds a flight route from some city in the input list to the ;; destination, or returns false if no such route exists (define (find-flights/list loc finish visited) (cond [(empty? loc) false] [else (local [(define possible-route (find-helper (first loc) finish visited))] (cond [(boolean? possible-route) (find-flights/list (rest loc) finish visited)] [else possible-route]))]))] (find-helper start finish empty))) The airline that asked for this program complains that the program spends lots of time computing the same routes over and over. Since their route map doesn't change very often, they want a program that keeps track of the routes that have already been computed. If someone requests a route that has already been computed, the program should return the pre-computed route. Otherwise, it should compute the route as before. How should we write this program? At first, this sounds like something we could write with an accumulator. This won't work though. Why not? An accumulator stores information over the course of one main call to a program, but we want to maintain information over many main calls to a program. In other words, we need programs with memory of how they've been called over time. Since find-flights is a large program, let's look at the same idea on a simpler program. Assume we have a program f which consumes a number and produces a number: (define (f x) (g (* x x))) Never mind what g does. Let's just assume that it's complicated and takes a long time to finish computing, hence we want to remember the output of previous calls to f. What do we need? First, we need a representation for previously computed values of f: ;; x-val and ans are both numbers (define-struct result (x-arg ans)) Next, we need a way to refer to the list of previously computed results. We'll introduce a variable called table and set it to the initial list of previously computed values. (define table empty) Now, we need to edit the definition of f to check for previously computed values: (define (f x) (local [(define prev-result (lookup x table))] (cond [(number? prev-result) prev-result] [else (local [(define x-result (g (* x x)))] ;; store result in table result)]))) ;; lookup : num (listof result) -> num or false ;; returns answer stored in table for arg, or false if no value stored (define (lookup arg table) (local [(define prev-ans (filter (lambda (res) (= arg (result-x-arg res))) table))] (cond [(empty? prev-ans) false] [else (result-ans (first prev-ans))]))) Finally, we need to change table so that it will remember this value of f. To do this, we need a new keyword. It's called set!. A set! expression takes a variable name and an expression. It changes the definition of the variable to refer to the new expression. To change the contents of the table, we would therefore write: (set! table (cons (make-result x x-result) table)) Thus, the final version of f with tables would appear as follows: (define table empty) (define (f x) (local [(define prev-result (lookup x table))] (cond [(number? prev-result) prev-result] [else (local [(define x-result (g (* x x)))] (set! table (cons (make-result x x-result) table)) result)]))) [plus the definition of lookup from above] This process, by which we give a program memory of which values it has already computed, is called memoization. There's one minor problem with this program as written: f is the only function that uses table, so we should hide table inside of f. How do we hide information? We use local. As a first attempt, we'll try the following: (define (f x) (local [(define table empty) (define prev-result (lookup x table))] (cond [(number? prev-result) prev-result] [else (local [(define x-result (g (* x x)))] (set! table (cons (make-result x x-result) table)) result)]))) Unfortunately, this doesn't work. Why not? Recall the semantics of local. When we evaluate (f 3), DrScheme creates a dummy variable for the local variables table and prev-result, then uses those dummy names in the body of the local. So, it's as if we had written (define dummy-table empty) (define dummy-prev-result (lookup 3 dummy-table)) (cond [(number? dummy-prev-result) dummy-prev-result] [else (local [(define x-result (g (* 3 3)))] (set! dummy-table (cons (make-result x x-result) dummy-table)) result)]))) This process will create a new table with a different dummy name each time we call f. So clearly, this approach doesn't work. We really need some way to hide the table inside of f _before_ consuming a particular value for x. We can do this if we use lambda: (define f (local [(define table empty)] (lambda (x) (local [(define prev-result (lookup x table))] (cond [(number? prev-result) prev-result] [else (local [(define x-result (g (* x x)))] (set! table (cons (make-result x x-result) table)) result)]))))) Why does this version work? DrScheme still creates a dummy variable for the table and replaces table with dummy-table inside the lambda expression: (define dummy-table empty) (lambda (x) (local [(define prev-result (lookup x dummy-table))] (cond [(number? prev-result) prev-result] [else (local [(define x-result (g (* x x)))] (set! dummy-table (cons (make-result x x-result) dummy-table)) result)]))) However, this creates the dummy table variable only once, then traps it inside the lambda. Since f refers to this lambda expression, every time we call f, we affect the value of dummy-table. Thus, dummy-table persists over each call to f, which is what we wanted. This example demonstrates something interesting about lambda expressions: they can capture the values of variables defined outside of their scope. We therefore refer to lambda expressions as closures, because they close over the values of variables defined outside of their scope. Let's step back and recap: what have we discussed today? 1. Sometimes, we want to equip programs with memory about the values that they computed previously. This process is called memoization. 2. In order to remember values, we need a way to change what a variable refers to over time. We do this with the Scheme keyword set!. 3. Use closures to create variables that will persist over several calls to a function. With this summary in hand, let's return to our original problem: we wanted to add memory to find-flights. Let's keep just the core of the program and modify it accordingly: (define (find-flights start finish) (local [(define rm ...) (define (find-helper start finish visited) ...) (define (direct-cities from-city) ...) (define (find-flights/list loc finish visited) ...)] (find-helper start finish empty))) What do we need to add to this program? We need a representation for pre-computed routes, a way to store the precomputed routes, a way to look for pre-computed routes, and a way to add newly computed routes. Here's the updated core (you can write lookup on your own): ;; start and finish are city and cities is (listof city) (define-struct route (start finish cities)) (define find-flights (local [(existing-routes empty)] (lambda (start finish) (local [(define rm ...) (define (find-helper start finish visited) ...) (define (direct-cities from-city) ...) (define (find-flights/list loc finish visited) ...)] (local [(route (lookup start finish existing-routes))] (cond [(cons? route) route] [else (local [(new-route (find-helper start finish empty))] (set! existing-routes (cons new-route existing-routes)) new-route)])))))) As a side note, there is one annoying thing about this program: it fixes the route map. What if another airline wanted to use this program? How could you edit the program to be reusable over many route maps, _without_ making the route map an argument to find-flights? Let's step back and look at how DrScheme evaluates set! expressions. First, what is the value of a set! expression? > (set! x 3) is an error because you cannot set! undefined variables. However, what if we defined x first? > (define x 1) > (set! x 3) > Notice that DrScheme doesn't print anything. That's because set! expressions have no visible value. You can think of them as having an invisible value, but one that you can't do anything with (since you can't see it). When DrScheme sees a set!, it behaves as if you changed the original definition of the set!-ed variable. So, if you write (define x 1) (set! x 3) then DrScheme treats this as (define x 3) after it reaches the set!. Here's a more complicated example: (local [(define x 1)] (begin (set! x 3) x)) Begin is another new keyword. We can use it to evaluate several expressions in turn. The value of the begin is the value of the last expression in the begin. Thus, begins only make sense if the last statement returns a value and the remaining statements use set!. Let's hand-evaluate this expression. After processing the local, we have (define dummy-x 1) (begin (set! dummy-x 3) dummy-x) When we process the begin, we evaluate each expression in turn and remove it from the begin. When we process the set!, we replace the old definition of dummy-x with the new definition as required by the set!. Thus, in the next step we'd get (define dummy-x 3) (begin dummy-x) Now, we just have to evaluate (begin dummy-x), which is equivalent to dummy-x, which is defined to be 3.