Recall the find-flights program that we wrote in the last class: ;; find-flights : city city route-map -> (listof city) or false ;; creates a path of flights from start to finish (define (find-flights start finish rm) (cond [(symbol=? start finish) (list start)] [else (local [(define possible-route (find-flights/list (direct-cities start rm) finish rm))] (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 rm) (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 route-map -> (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 rm) (cond [(empty? loc) false] [else (local [(define possible-route (find-flights (first loc) finish rm))] (cond [(boolean? possible-route) (find-flights/list (rest loc) finish rm)] [else possible-route]))])) The program find-flights/list uses a common algorithmic technique that we call backtracking. In backtracking, we try one potential solution to a solution, then go back and try another potential solution if the first one fails. You'll see this technique come in handy on many problems. What's the termination argument on find-flights? Each recursive call looks for a route that uses fewer flights. Eventually, each path must eventually end in the finish city or have no outgoing flights. This program works fine on our initial route map. Let's try the following route map, which adds a flight from Dallas to Houston: (define routes2 (list (make-city-info 'Houston (list 'Dallas 'NewOrleans)) (make-city-info 'Dallas (list 'Houston 'LittleRock Memphis)) (make-city-info 'NewOrleans (list 'Memphis)) (make-city-info 'Memphis (list 'Nashville)))) Let's write down the calls that occur when we try to find a route from Houston to Memphis in the new route map. (find-flights 'Houston 'Memphis routes2) (find-flights/list (list 'Dallas 'NewOrleans) 'Memphis routes2) (find-flights 'Dallas 'Memphis routes2) (find-flights/list (list 'Houston 'LittleRock 'Memphis) 'Memphis routes2) (find-flights 'Houston 'Memphis routes2) ... Notice that we end up with an infinite loop. What happened? First, this tells us that our termination condition was wrong. Our previous condition assumes that there are no cycles in the route. However, here's a map where we do have cycles, and it's clear that our approach won't terminate. Therefore our current program only works on route maps with no cycles. Why does the program break on cycles? Because our program has no knowledge about which cities it has already tried. Each recursive call is independent of all of the others. In this problem, however, we want to remember which cities we have already tried. How could we give our program this knowledge? We could add a parameter to find-flights that stores the cities that have already been visited. If we attempt to find flights from a city on the list, find-flights should return false to truncate the search through that path. Here's the new code: ;; find-flights : city city route-map (listof city) -> (listof city) or false ;; creates a path of flights from start to finish (define (find-flights start finish rm visited) (cond [(symbol=? start finish) (list start)] [(memq start visited) false] [else (local [(define possible-route (find-flights/list (direct-cities start rm) finish rm (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 rm) (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 route-map (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 rm visited) (cond [(empty? loc) false] [else (local [(define possible-route (find-flights (first loc) finish rm visited))] (cond [(boolean? possible-route) (find-flights/list (rest loc) finish rm visited)] [else possible-route]))])) What's the termination argument now? Each city can serve as the start to at most one search for flights. Since we have a finite number of cities in the graph, the search must eventually stop. We call a parameter like visited an accumulator, because it accumulates information over the course of a computation. In the next few classes, we're going to look at various aspects of designing programs that use accumulators. There is one small problem with the above solution: it changed the contract on find-flights. Could we avoid that? Yes, by using a helper function. This helper function, new-find-flights, has the same contract as the original find-flights. However, it controls the initial value passed to visited, which prevents someone from calling your code with the wrong initial value. ;; new-find-flights : city city route-map -> (listof city) or false ;; creates a path of flights from start to finish (define (new-find-flights start finish rm) (find-flights start finish rm empty)) As an exercise, use local to hide the accumulator version of find-flights inside new-find-flights. Where can you refrain from passing parameters in this new version? Let's turn to another example. A company maintains a list for each employee of how many sick days that person earned or used in each month. For example, (list 2 -1 1) indicates that a person earned 2 sick days the first month, used 1 in the second month, and earned 1 in the third month. Employees are allowed to accumulate sick days. The company needs a program that consumes the list of sick days earned or used per month and returns a list showing how many sick days the employee has available at any given month. ;; available-days : (listof number) -> (listof number) ;; computes sick days available per month based on those earned per month (define (available-days alosd) ...) (available-days (list 1)) = (list 1) (available-days (list 2 -1 1)) = (list 2 1 2) Let's write a purely structural recursive solution first. ;; available-days : (listof number) -> (listof number) ;; computes sick days available per month based on those earned per month (define (available-days alosd) (cond [(empty? alosd) empty] [else (cons (first alosd) (add-to-each (first alosd) (available-days (rest alosd))))])) (define (add-to-each n alon) (map (lambda (x) (+ x n)) alon)) What are the calls that result when we compute (available-days (list 2 -1 1))? (available-days (list 2 -1 1)) = (cons 2 (add-to-each 2 (available-days (list -1 1)))) = (cons 2 (add-to-each 2 (available-days (list -1 1)))) = (cons 2 (add-to-each 2 (cons -1 (add-to-each -1 (available-days (list 1))))) = (cons 2 (add-to-each 2 (cons -1 (add-to-each -1 (cons 1 (add-to-each 1 (available-days empty)))))) Notice how often we end up traversing the lists in order to process all the calls to add-to-each. Consider the last element in the list. By the time we finish this program, we will end up adding all of the previous elements to the last element. Can we use an accumulator to reduce the amount of traversing that we need to do? ;; avail-days-accum : (listof number) num -> (listof number) ;; uses an accumulatpr tp compute sick days available per month based ;; on those earned per month (define (avail-days-accum alosd accum-days) (cond [(empty? alosd) empty] [else (cons (+ (first alosd) accum-days) (avail-days-accum (rest alosd) (+ (first alosd) accum-days)))])) How should we call the new function? (avail-days-accum (list 2 -1 1) 0) As we did for new-find-flights, we should define a function with the same contract as the original function. Our accumulator version should be defined as a local function within the new function. ;; available-days : (listof number) -> (listof number) ;; computes sick days available per month based on those earned per month (define (available-days alosd) (local [(define (avail-days-accum alosd accum-days) (cond [(empty? alosd) empty] [else (cons (+ (first alosd) accum-days) (avail-days-accum (rest alosd) (+ (first alosd) accum-days)))]))] (avail-days-accum alosd 0)))