Rice University

COMP 210: Principles of Computing and Programming

Lecture #31       Fall 2003

set-[struct]-[field]!

Here's some demo code that shows an example of the above ideas: drawshapes.scm (this code uses the "begin" syntax, which is described here.)

So, what does the hand-evaluation of

(cons 3 (cons 4 (cons 5 empty)))
look like? How about for
(cons (make-cat 'morris 93) (cons (make-cat 'garfield 87) empty))
What if , as in previous lectures, cats had an additional field bestbuddy refering to a cat?

Exercise: make the smallest example of you can, of a structure which somehow contains a reference to itself. (How does it print out? Remember shared from last lecture.)

Graphs ("trees with cycles")

(tentative)

How would we represent a map of the US, before we had mutating structures? How can we make them more properly, now?

(define-struct city (name nhbrs seen?))
;;
;; A city is a 
;; (make-city [symbol] [list-of-cities] [boolean])
;; where seen? is to be used later, in path-finding.

; Initially no neighbors:
(define nyc  (make-city 'nyc  empty false))
(define la   (make-city 'la   empty false))
(define slc  (make-city 'slc  empty false))
(define hou  (make-city 'houston empty false))
(define nome (make-city 'nome empty false))
(define reno (make-city 'reno empty false))

(define cities (list nyc la slc hou nome reno))


(set-city-nhbrs! nyc  (list la slc hou nome))
(set-city-nhbrs! la   (list reno slc nyc))
(set-city-nhbrs! hou  (list nyc la))
(set-city-nhbrs! slc  (list reno))
(set-city-nhbrs! reno (list slc))

Note that the connections between cities are one-way. You can't always get back the way that you got somewhere.

What is our algorithm for searching?

;; path: city, city --> list-of-city or false
;; Return false if no path, otherwise return
;; a path which includes both endpoints.
;;
(define (path src dest)
  (cond [(eq? src dest) (list src)]
        [else (local {(define otherways (map (lambda (s) (path s dest))
                                        (city-nhbrs src)))
                      (define viable-otherways (filter list? otherways))}
                (cond [(empty? viable-otherways) false]
                      [else (cons src (first viable-otherways))]))]))



;;; Alternate version of path, w/o using map or fold:
;;
(define (path src dest)
  (cond [(eq? src dest) (list src)]
        [else (local {(define otherway (path-from-list (city-nhbrs src) dest))}
                 (if (list? otherway)
                     (cons src otherway)
                     false))]))

;; path-from-list: list-of-city, city --> list-of-city or false
;; Return either a path from one of srcs to dest,
;; or false if no such path exist from anybody in srcs.
;;
(define (path-from-list srcs dest)
  (cond [(empty? srcs) false]
        [else (local [(define try-first (path (first srcs) dest))]
                 (if (list? try-first)
                     try-first
                     (path-from-list (rest srcs) dest)))]))

(path slc nyc) = 
;;  Uh-oh, runs forever!
;;  Hand-evaluation reveals
;;  It's checking slc, reno, slc, reno, slc, ...

The above code uses the if statement, which is described here.

N.B. Instead of a separate function path-from-list, we could instead mapped "find a path to dst" on each of the cities neighbors, and then grabbed the first element from all these solutions (or, checked if map returned a list containing only false from each neighbor). Same diff.

What do we need to do, to avoid loops?

(The function is called path3 because the DrScheme file has all the path functions in it. --- path2 vaporized somewhere in history.)
;; path3: city, city --> list-of-city or false
;; Return false if no path, otherwise return
;; a path which includes both endpoints.
;;
(define (path3 src dest)
  (cond [(eq? src dest) (list src)] ;; trivial soln
        [(city-seen? src) false] ;; trivial soln. Already visited, so don't go here again.
        [else 
         (begin
           (set-city-seen?! src true)  ;; don't revisit this city
           (local {(define otherways (map (lambda (s) (path3 s dest))
                                          (city-nhbrs src)))
                   (define viable-otherways (filter list? otherways))}  ;; recursive result
             (cond [(empty? viable-otherways) false]   ;; check if no paths
                   [else (cons src (first viable-otherways))])))]))  

(path3 slc nyc) ;; -> false
(path3 nyc nyc) ;; -> (nyc)
(path3 nyc slc) ;; -> (nyc la slc), printed in "shared" form.
 ; Note, it's not the shortest path.

This is a "depth-first search": it searches the first neighbor, and the first of that neighbor, and ... and backtracks when it reaches a dead end (or something seen before). Note that this does not guarantee that the shortest route is found first (why?).

 

 

 

The above code only tries the first city in the neighbors list.

So, let's try to find all the possible non-looping paths by checking all the neighbor cities (it's called path4 because the DrScheme file has all the path functions in it as well). Our function will now return a list of paths from the src to the dest:


;;path4: city city -> list-of-list-of-cities
;; returns a list of paths from src to dest.
;; returns empty if no paths.
;; The paths include src and dest.
(define (path4 src dest)
  (cond
    [(eq? src dest) (list (list src))]  ;; trivial soln
    [(city-seen? src) empty]            ;; trivial soln
    [else
     (begin
       (set-city-seen?! src true)   ;; don't re-visit this city
       (map (lambda (a-path) (cons src a-path)) ;; add this city
            (foldr (lambda (a-city rr)  ;; recursive call
                     (append (path4 a-city dest) rr))
                   empty
                   (city-nhbrs src))))]))

;; The first thing we find is that this code won't run properly.    
;; It gives back empty every time.
;; Why?










;; The seen? fields need to be reset to true:

;; reset-cities: --> void
;; resets the seen? field to false
;; in all the cities
(define (reset-cities)
(begin
(set-city-seen?! nyc false)
(set-city-seen?! la false)
(set-city-seen?! hou false)
(set-city-seen?! slc false)
(set-city-seen?! reno false)))
(reset-cities) ;; reset all the seen? fields to true (path4 nyc slc) ;; --> ((nyc slc) (nyc hou la slc))

Better, but still not all of the routes. In particular, why did this routine not return the route the previous function did?

 

 

An unintentional side-effect of setting the seen? field of a city to true is to prevent the algorithm from looking at all the possible routes. The (nyc la slc) route is not considered because la has already been seen via the (nyc hou la slc) route.

Well, maybe if we clean up the code a bit by replacing the foldr with a an apply append and a map. The generative recursion template will clearly emerge when we do this:

;;path5: city city -> list-of-list-of-cities
;; returns a list of paths from src to dest.
;; returns empty if no paths.
;; The paths include src and dest.
(define (path5 src dest)
  (cond
    [(eq? src dest) (list (list src))]  ;; trivial soln
    [(city-seen? src) empty]            ;; trivial soln
    [else
     (begin
       (set-city-seen?! src true)  ;; no re-visiting of this city
       (map (lambda (a-path) (cons src a-path))  ;; add this city
            (apply append                       ;; combine results
                   (map (lambda (a-city)        ;; recursive call
                          (path5 a-city dest))
                        (city-nhbrs src)))))]))
						
(path5 nyc slc) ;; -> ((nyc la slc) (nyc slc))

ARGHHHHH!!!! We got a different set of routes, which still isn't the complete set! Why?

 

 

 

 

foldr does reverse accumulation while map is optimized for forward accumulation, so they traverse the neighbors list in different directions. The problem of accidentally cutting off viable routes still exists.

What we really want is to set the seen? field to true only while we are calculating the recursive result, not for the entire running of the function. Thus we have to restructure our code a bit so we can add in some clean-up code that sets the seen? field back to false after we are done:

;;path6: city city -> list-of-list-of-cities
;; returns a list of all non-looping paths from src to dest.
;; returns empty if no paths.
;; The paths include src and dest.
(define (path6 src dest)
  (cond
    [(eq? src dest) (list (list src))]  ;; trivial soln
    [(city-seen? src) empty]            ;; trivial soln
    [else
     (local
         [(define rr   ;; get the recursive result
            (begin
              (set-city-seen?! src true) ;; don't re-visit this city in the recursive call
              (map (lambda (a-path) (cons src a-path)) ;; add this city
                   (apply append                 ;; combine results
                          (map (lambda (a-city)  ;; recursive call
                                 (path6 a-city dest))
                               (city-nhbrs src))))))]
       (begin
         (set-city-seen?! src false)  ;; clean up mutation
         rr))])) ;; return recursive result


(reset-cities)  ;; still have to reset the cities before the first run
(path6 nyc slc) ;; --> ((nyc la reno slc) (nyc la slc) (nyc slc) (nyc hou la reno slc) (nyc hou la slc))

;;note that this version can run again without resetting the seen? fields.

Finally! We got all the possible routes this time. But we were lucky that we could analyze the whole system by hand to find out what unwanted side effects our mutation was having. In a large, complex system, this may not be possible, making this sort of bug extremely difficult to track down.

Advice: Avoid using mutation unless you absolutely need it and then do everything you can to limit the extent of its effect.

(Here's the code for the depth-first path searching)

A fundamentally different way of finding a path from one place to another is the "breadth-first search": look at all places one step away from src, then two steps away, then three steps, ... This method is guaranteed to always return a shortest path (can you seen why?).

 

Written mostly by Ian Barland.

 

©2003 Stephen Wong