|
Comp210: Principles of Computing and Programming
|
set-[struct]-[field]!
(define-struct [struct] (... [field] ...))Then there is a corresponding function
(set-[struct]-[field]! [a-struct-expr] exp)
Semantics of make-[struct]
: Instead of going
off and creating a chest-of-drawers with the indicated contents and
returning that chest-of-drawers (the structure), we refine this:
make-[struct]
creates the structure but returns a reference to that
structure. References are a new type of value; in particular we don't
pass structures to other functions, but only references to structures.
(We have no way of ever getting hold of the structure itself; most of
the time we'll just say "the structure ..." rather than "a reference to
the structure...", for convenience.)
When hand-evaluating make-[struct]
, we will
draw a box (the created structure), and draw a signpost with an arrow
referring to the box (the reference). (Sometimes, instead of the box,
we'll just write the make-[struct]
, and draw the
reference to it.) (If in full-text mode, we'll write
(define somereference^ (make-struct ...))and understand that
somereference^
is the equivalent of the
arrow referring to that make-struct.
Example:
(define-struct cat (name age)) (make-cat 'morris 93)) (define cat2 (make-cat 'garfield 87)) (define cat3 cat2)will hand-evaluate to:
(make-cat cat1 (make-cat 'morris 93)) = (define randomcat^ (make-cat 'morris 93)) (define cat2 (make-cat 'garfield 87)) = { (define somecat^ (make-cat 'garfield 87)) ; make-cat created the struct, { ; and made the reference to it. { (define cat2 somecat^) ; Make-cat returned a reference. (define cat3 cat2) = (define cat3 somecat^) ; Evaluate cat2, as usual: it's a placeholder; ; just look up the placeholder's define'd value.Note that the
(define somecat^ ...)
is a bit of abuse of
notation: it's not a real define (since we can only define placeholders,
but the somecat^
we said is a reference value (a signpost).
set-[struct]-[field]!
Evaluate [exp]
to get val, evaluate
[a-struct-expr]
to get strct and then
follow the reference strct (it had better be (a reference to)
a struct of type [type]
, of course), and modify
the [field]
to be val. Note that no
placeholders were modified in this process.
set-[struct]-[field]!
Call this a constructor (make-[struct]
) once
for each corresponding thing you're modeling. That is, if you're working
at a vet clinic and you have two separate cats each named 'morris
and age 93
, then have two calls to make-cat
.
(If you are repeatedly seeing the same cat, only create it once and use
that same value in the future: either name it with a placeholder, or
keep a list of cat structures (retrieving the right cat from the list as
needed, etc.)
Call the mutator set-[struct]-[field]!
set-cat-age!
to modify the age
field.)
What about translate-posn -- is it appropriate to change the object's fields, or to return a new posn? Probably the latter; we don't think of points themselves as changing. If something has a position, we might change it's position, but the old position doesn't change.
(define origin (make-posn 0 0)) ; Recall: rectangle structures contain a posn as their NW corner, ; and circle structures contain a posn for their center. ;; Character-shapes for my rad new videogame. ;; They both start out at Secret Base Headquarters: ;; (define blocko (make-rectangle origin 10 30 'blue)) (define blobbo (make-circle origin 20 'red))What if we want to translate one of these two shapes? But if we have a Rectangle structure (which contains a posn as its northwest corner), and we have the idea of this Rectangle being moved around, then we might well call
set-Rectangle-nw!
, installing an entirely new posn. [Draw pictures!]
What happens if you used
(set-posn-x! (Rectangle-nw blobbo) ..)
; and other structures
also had a reference to that same posn? Suppose a structure had several
posn's involved (e.g. (define-struct alien-body head legs)
,
where head and legs were each shapes, and should be moved in synchronicity?
What if the alien gets blasted to smithereens?
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.)
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 map
ped "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?
;; 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.
(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.
Last Revised Tuesday, 24-Aug-2004 13:49:04 CDT
©2004 Stephen Wong and Dung Nguyen