Here's the definition of an entry:
(define-struct entry (key val))
And a useful helper function:
;; have-key? key entry --> boolean
;; Returns whether the key is the same as that of the entry.
(define (have-key? key entry)
(equal? key (entry-key entry)))
The following example won't use visitors, just in case they
would distract from the points of the lab.
If you have time, go back and redo the exercises using visitors.
Also, in these examples, adding an entry will modify the current
dictionary. Contrast this with, e.g., the in-class stack example
which created a new stack when you added an element.
-
Create a global list of entries, named contents ,
which will be the "datastore" for our dictionary.
Then, finish writing the two functions
lookup and add! below.
This should be really easy.
;; add!: ANY ANY --> list-of-entries
;; SIDE-EFFECT: change the global placeholder "contents".
;; We return the updated contents.
;;
(define (add! key val)
…)
;; lookup: key --> entry or false
;; Does the key occur inside "contents"?
;; If so, return that entry, else return the sentinel false.
;;
(define (lookup name)
(local [;; helper: list-of-entries --> entry or false
;; Does the key occur inside "entries"?
;; If so, return that entry, else return false.
;;
(define (helper entries)
(cond [(empty? entries) …]
[(cons? entries) …]))]
(helper contents)))
Sample solution.
-
The above code should work and test okay.
However, it has a couple of huge drawbacks:
-
Anybody might set!
contents directly.
Even if someone is trying to use contents
correctly, they could easily make mistakes.
E.g., if we were keeping it in sorted order
as a way to improve lookup 's efficiency,
everything could get screwed up.
-
We can only make one single dictionary, ever!
We'd certainly like to write a program which
used several independent dictionaries.
We'll fix these flaws by encapsulating the
contents and two functions in a single data structure.
However, rather than making the contents explicit,
we will hide it in the closures of the two functions.
;; A Dictionary is a structure with two "methods"
;; (fields which are functions):
;; add-fn! : ANY ANY --> list-of-entries
;; SIDE-EFFECT: change the contents.
;; lookup-fn: key --> entry or false
;; Does the key occur inside "contents"?
;; If so, return that entry, else return the sentinel false.
;;
(define-struct dict (add! lookup))
Write a function dict-Factory ,
which takes no arguments
but returns a new, independent dictionary that initially
has no entries.
Make sure you can create several dictionaries which
don't interact.
Sample solution.
-
Suppose we'd like to keep track of how many dictionaries
get made.
We need to keep track of this information, and give
access to it, so that it is useful.
-
Use a global variable
num-dicts to keeps track of this.
Add another field to dictionaries, id ,
so that the first dictionary has an
id of 1, the second an id
of 2, etc.
Making new dictionaries should not change the
id s of existing dictionaries, of course.
-
Discuss with your partner:
What are the drawbacks of making this
id field a number
rather than a function returning a number?
If your code is going to be used by many
other programmers, how easy is it (intentionally
or accidentally) for them to subvert your data?
-
Then, the tricky part, make this variable
not be global.
But, clearly it does need to have
dict-Factory inside its scope,
and we only want one variable,
not one per call to dict-Factory .
Hint:
(define dict-Factory
(local […]
(lambda () …)))
Sample solution.
-
Note that noone using your dictionary knows that
you are using a list-of-Entries to store your data;
they just call the add!-fn and
lookup-fn .
But, other programmers tell you that they often
use your dictionary and would like it to be faster.
Furthermore, they also tell you that
the keys are natural numbers in some bounded range:
say, 1..100 for the programmer keeping track of grades,
and 1..1000000 (Rice student ID) for the
Registrar's database, or 100..800 for MUSI course numbers.
We can make a dictionary which is optimized for
quick lookups in this case.
Rather than lists, we can use vectors.
Write the factory below:
;; dict-n-Factory: natnum --> dictionary
;; Returns a dictionary where keys are natural numbers
;; in the range 0..n-1.
;;
(define (dict-n-Factory num-keys)
…)
For the moment, write this assuming the key will
always be in range.
Sample solution.
-
To be fair, the previous function doesn't quite
return a true dictionary,
since dictionaries allow the key to be anything.
Modify your previous function so that if the key is not
a natural number in range, it still works.
How? Well, we could have our dictionary contain
both a vector (to hold all the numeric keys
in range) and it a list (for the rest).
Hint: You could copy/paste code from
dict-Factory .
But we hate repeating code, remember?
How about, having your dict-n-Factory
secretly containing not just a vector (for numeric keys
in range), but also a whole other true dictionary (for
for the rest).
Sample solution.
-
Optional:
Let's assume all our keys will be natural numbers.
Vectors are great when we know how many entries
will be used in advance.
But what if we don't? What if somebody provides
a numeric key greater than or equal to
num-keys ?
We want the unbounded nature of a list,
with the random-access nature of a vector.
Here's a clever trick so that we can have our cake
and eat it too.
Start with a vector of size (say) 100.
For keys of size less than 100, no problem.
If we eventually get a key that is 100 or bigger,
then we'll make a whole new vector of size 200,
copy over the contents of our size-100 vector,
keeping only the new, bigger vector!
This "repeated doubling" technique is handy.
Theoretically, it averages out to be
just as fast as a regular vector implementation.
Write such a new version.
Sample solution.
|