random
)
You are a disgruntled programmer
(they have you writing a function even called toil-away
),
and you decide to sabotage
your code. Namely, you want it to run correctly the first
500 times its called, and then stop.
[Disclaimer: Do not try this yourselves.]
Well, you realize that this is an example where we want to maintain some
history from previous calls.
So like the random
function,
we'll have a placeholder:
(define times-called 0) (define (toil-away ...) (begin (set! times-called (add1 times-called)) (if (> times-called 500) 'hahaha----look-who-is-laughing-now ; Now, the code which actually does what it should. ...)))(Yes, it's wrong to have the magic constant 500 hard-coded in there, but that pales in comparison to what the function does.)
The customers get a teachpack containing the code,
but they can't modify the teachpack, so you think your plan is fine.
However, there's one problem:
after your colleagues realize what you did,
the customers don't even need to download a new version -- customer
support tells them "just type (set! times-called 0)
and things will work again.
next month's version will have a feature where you won't even need
to do this;
you can upgrade for only $99!"
Question:
How can you work this so that times-called
isn't accessible to everybody?
Answer:
We can just use local, and the function
w/o revealing times-called
:
(define toil-away (local [(define times-called 0)] ; Now, the body of the local: a function. (lambda (..) (begin (set! times-called (add1 times-called)) (if (> times-called 500) 'hahaha----look-who-is-laughing-now ; Now, the code which actually does what it should. ...)))))Now
toil-away
is the function we want,
but times-called
is inaccessible
outside of that local
.
local
:
it renamed
times-called
to be
times-called%473
(which includes an invisible character impossible to type,
so this name is unique, past and future),
and really toil-away
refers to
the placeholder
times-called%473
.
Question:
How many times does drscheme encounter the local?
Just once (even if calling the lambda-function many times).
Next question:
What if the local had instead been mis-placed inside the lambda?
(how many times does drscheme encounter the local in that situation?)
Let's revisit the phonebook for an extended example.
We previously made it possible for
two functions
add-entry!
and lookup
to share the same state
via the variable directory
, a list of entries.
One problem though:
directory
was a variable anybody could modify.
We don't want people who buy our code to go mucking with
directory
directly,
since we depend on its properties (e.g. perhaps no duplicate entries)
in our code.
We want to foolproof our code, so they can't break it even if they try.
You might think "go ahead give them the power to change the code;
if they do something stupid that's their fault."
But one lesson that has been learned in the past:
if other programmers have the power to mis-use something,
they will, and it result in buggy programs and crashes.
By the way, we're not only foolproofing it from customers,
but also from teammates who might change variables not realizing
they're violating some invariant, and from yourself in the future.
Okay, back to solving the problem.
We could use the exact same approach as in
toil-away
,
except that we need to return (a list of) two functions from
the body of the local
,
and then just give public names to the first and second
items of that list.
The local renames the variable directory
so that
those two functions now each refer to directory%876
,
a renamed variable we can't access.
But somebody mentioned last time,
a way to do solve the problem slightly differently.
Rather than export all the utility functions outside the local,
we'll export a single function
phonebook-send
which simply calls
the other functions as needed.
That is:
;; campus-phonebook-send: symbol, list-of-args --> ANY ;; Take in one of the symbols ;; 'do-lookup ;; 'add-new-entry ;; and a list of arguments for the indicated function, ;; and we'll do the requested thing. ;; (define campus-phonebook-send (local [(define directory empty) (define (lookup name) ...directory...) (define (add-entry! name number) ...(set! directory (cons .. directory))...)] ; Now, as the body of the local, return the dispatch function: (lambda (msg args) (cond [(symbol=? msg 'do-lookup) (lookup (first args))] [(symbol=? msg 'add-new-entry) (add-entry! (first args) (second args))] [else (error 'phonebook-send (format "~s not a phonebook method (~s).~n" msg args))])))) ;; Examples of use: (campus-phonebook-send 'do-lookup (list 'ian)) = false (campus-phonebook-send 'add-new-entry (list 'ian 3843)) ; = ...the directory (campus-phonebook-send 'do-lookup (list 'ian)) = 3843 directory ; ERROR undefined symbol "directory" -- it was local!This approach also solves the problem of sharing directory between the different functions, but hiding it from outsiders.
It used a different sort of perspective: rather than saying "call the function lookup, and pass it a phonebook", we are saying "take the phonebook, and tell it to do a lookup." This perspective is called object-oriented: the object is fundamental, it contains data (like a structure) but it also contains functions ("methods") inside of it, and we can have it run any of its methods.
If that's all there was to object-orient programming,
it would be boring (just a superficially different way of
calling functions), although it does use local
in a way to provide data-hiding,
which is a good thing.
Also note that the dispatch function can
be written automatically by the computer itself.
(All object-oriented languages do this.
Later we'll visit scheme's objects proper, in the last week of class.)
Our phonebook proves so successful on campus, that SWBell wants to use our code for its houston and austin phonebooks.
(define houston-phonebook-send ...) (define austin-phonebook-send ...)Okay, we could copy-and-paste the above local into "...", but that's ugly. Wait, we had named the first local, the name "campus-phonebook-send". Can we just insert that name for the "..."s? Of course not -- we want separate phonebooks for each city. (This only would've worked before we introduced
set!
.)
Aha, of course, we can make a function that returns a phonebook:
;; new-phonebook: --> phonebook-object ;; Where a phonebook-object is a function ;; like we had before (e.g. campus-phonebook was ;; one particular phonebook-object); ;; it understands the messages 'do-lookup and 'add-a-new-entry. ;; (define (new-phonebook) (local [(define directory empty) (define (lookup name) ...directory...) (define (add-entry! name number) ...(set! directory (cons .. directory))...)] ; Now, as the body of the local, return the dispatch function: (lambda (msg args) (cond [(symbol=? msg 'do-lookup) (lookup (first args))] ...)))) (define campus-phonebook-send (new-phonebook)) (define houston-phonebook-send (new-phonebook)) (define austin-phonebook-send (new-phonebook)) ;; Examples of use: (campus-phonebook-send 'do-lookup (list 'ian)) = false (campus-phonebook-send 'add-new-entry (list 'ian 3843)) = ;... (campus-phonebook-send 'do-lookup (list 'ian)) = 3843 (austin-phonebook-send 'do-lookup (list 'ian)) = false ; ian not in austin !-(
How does this work, exactly?
It all works fine, thanks to local
's renaming.
Take a moment to hand-evaluate the above functions, to
see how the stepper ends up with
several top-level
definitions of renamed placeholders,
and renamed functions using specific placeholders:
(define directory%001 empty) (define lookup%001 (lambda (name) ...directory%001...)) (define add-entry!%001 (lambda (name number) ...(set! directory%001 ..) ...)) (define directory%002 empty) (define lookup%002 (lambda (name) ...directory%001...)) (define add-entry!%002 (lambda (name number) ...(set! directory%001 ..) ...)) ... (define campus-phonebook-send (lambda (msg args) (cond [...lookup%001...add-entry!%001...]))) (define houston-phonebook-send (lambda (msg args) (cond [...lookup%002...add-entry!%002...])))Thus we see that different phonebooks are using slightly different functions -- namely, functions which refer to different (renamed) directories!
Some more terminology:
we call the type "phonebook", as described
in the purpose of new-phonebook
, a class.
That is, we had one phonebook class,
and we made three instances of it
(e.g. we made three objects, each of type phonebook).
The function new-phonebook
is called the constructor
for its class.
Aside: Inheritance
We could think of having different variations of phonebooks -- perhaps later we'll implement a "phonebook-plus(tm)" which allow reverse lookups. That is, they accept all the same messages as a regular phonebook, but also accept additional messages. We'd say that phonebook-plus(tm) is a new class which extends the original phonebook class (or: it inherits from the phonebook). Note that any function which took in an (old, original) phonebook as an argument can also accept a phonebook-plus(tm), and will work fine! (This is the power of inheritance.) (How might you writenew-phonebook-plus<tm>
, which returns new phonbook-plus(tm)s, without having to re-write any code you already wrote, (but still keeping the old type of phonebook around)?)
Finally: Imagine a functionphonebook-factory
, which could make either regular old phonebooks, or (for customers who pay more money), it can make phonebook-plus(tm)s. This is called "the factory pattern".
Okay, one last exercise about using local variables and functions. Suppose we want to have each phonebooks have an ID number. so campus-phonebook-send and houston-phonebook-send can each be queried as to what their ID number is; the first will reply "1" while the other will reply "2".
(campus-phonebook-send 'get-id-num) = 1 (houston-phonebook-send 'get-id-num) = 2 ((new-phonebook) 'get-id-num) = 4 ((new-phonebook) 'get-id-num) = 5 (campus-phonebook-send 'get-id-num) = 1How can we do this? Well, clearly each phonebook has not only a local
directory
variable,
but also a local id-num
, which is defined when the
phonebook is created.
(local [(define directory empty) (define (lookup name) ...directory...) (define id-num ids-so-far) ; Worry later 'bout maintaining ids-so-far. ;...And of course we'll understand the message
get-id-num
,
which simply returns that variable.
(lambda (msg args) (cond [(symbol=? msg 'do-lookup) (lookup (first args))] [(sumbol=? msg 'get-id-num) id-num] ;...But how to make it so
new-phonebook
keeps track
of ids-so-far
, the next id number to give out?
Oh wait, this is nothing more than the principle we saw at the first
of the lecture -- keeping some state around about how many times
the function has been called.
We'll slip a local
inbetween the define
and the lambda
, which defines that variable.
Every time new-phonebook
is actually called,
in addition to returning a function (the phonebook object),
we first increment our counter.
;; new-phonebook: --> phonebook-object ;; ... ;; (define new-phonebook (local [(define ids-so-far 0)] ; Now the body of the local, which defines new-phonebook: ; a function which increment ids-so-far, and returns a phonebook object. (lambda () (begin (set! ids-so-far (add1 ids-so-far)) ; Now, the new phonebook function(object), in the body of this local: (local [(define directory empty) (define id-num ids-so-far) ..] (lambda (msg args) (cond [(symbol=? msg ...) ...]...)))))))Pay particular attention to how
ids-so-far
is looked at whenever we create a phonebook object,
inside the local.
In this example,
ids-so-far
was consulted only upon creating phonebooks
(and each phonebook remembered what the variable had been when it
was created).
But in general, there is nothing stopping
the functions like lookup
from making use of the current value of ids-so-far
.
Note that this value would be shared by all the different
phonebooks in existence!
(But inaccessible to anybody else.)
Terminology (will not be tested):
Since id-so-far
is local to the class
(i.e. to all phonebooks),
it's
called a static variable,
or class variable.
(Keep in mind that this is very different from
the member variables like directory
,
where each phonebook has their own separate version.)
Aside: Interestingly, the interplay between local, lambda, and set! is make these objects behave like structures -- we write our own functions to get (and set) the fields (member variables), and these changes can be seen by anybody who has a hold of the lambda.
Exercise:
The idea was to have shopper-card objects (one per customer),
which keeps track of their most recently bought item.
The following code fails to do this.
(define make-shopper-card (local [(define most-recently-bought #f)] (lambda (name) (lambda (command some-item) (cond [(eq? command 'show-name) name] [(eq? command 'buy) (set! most-recently-bought some-item)] [(eq? command 'show-fave) most-recently-bought])))))
;; toil: --> 'hi-ho-hi-ho-off-to-work ;; BUT sabotaged to only work for 3 times before breaking. ;; (define toil-away (local [(define times-called 0)] ; Now, the body of the local: a function. (lambda () (begin (set! times-called (add1 times-called)) (if (> times-called 3) 'hahaha----look-who-is-laughing-now ; Now, the code which actually does what it should: 'hi-ho-hi-ho-off-to-work))))) (toil-away) (toil-away) (toil-away) (toil-away) (toil-away) ;;;;;;;; Phonebook, version 1 (one lone phonebook). ;; campus-phonebook-send: symbol, list-of-args --> ANY ;; Take in one of the symbols ;; 'do-lookup ;; 'add-new-entry ;; and a list of arguments for the indicated function, ;; and we'll do the requested thing. ;; (define campus-phonebook-send (local [(define directory empty) (define (lookup name) (assoc name directory)) (define (add-entry! name number) (set! directory (cons (list name number) directory)))] ; Now, as the body of the local, return the dispatch function: (lambda (msg args) (cond [(symbol=? msg 'do-lookup) (lookup (first args))] [(symbol=? msg 'add-new-entry) (add-entry! (first args) (second args))] [else (error 'campus-phonebook-send (format "~s not a phonebook method (~s).~n" msg args))])))) ;; Examples of use: (campus-phonebook-send 'do-lookup (list 'ian)) = false (campus-phonebook-send 'add-new-entry (list 'ian 3843)) ; = ... (campus-phonebook-send 'do-lookup (list 'ian)) = (list 'ian 3843) ;directory ; ERROR undefined symbol "directory" -- it was local! ;;;; Phonebooks version two (multiple independent phonebooks) ;; new-phonebook: --> phonebook-object ;; Where a phonebook-object is a function ;; like we had before (e.g. campus-phonebook was ;; one particular phonebook-object); ;; it understands the messages 'do-lookup and 'add-a-new-entry. ;; (define (new-phonebook) (local [(define directory empty) (define (lookup name) (assoc name directory)) (define (add-entry! name number) (set! directory (cons (list name number) directory)))] ; Now, as the body of the local, return the dispatch function: (lambda (msg args) (cond [(symbol=? msg 'do-lookup) (lookup (first args))] [(symbol=? msg 'add-new-entry) (add-entry! (first args) (second args))] [else (error 'phonebook (format "~s not a phonebook method (~s).~n" msg args))])))) ;; send: (symbol, list-of-any --> any), symbol, list-of-any --> any ;; A superficial function to stress that we're sending ;; a message to an object. ;; (define (send object message args) (object message args)) (define campus-phonebook (new-phonebook)) (define houston-phonebook (new-phonebook)) (define austin-phonebook (new-phonebook)) ;; Examples of use: (send campus-phonebook 'do-lookup (list 'ian)) = false (send campus-phonebook 'add-new-entry (list 'ian 3843)) ;= ... (send campus-phonebook 'do-lookup (list 'ian)) = (list 'ian 3843) (send austin-phonebook 'do-lookup (list 'ian)) = false ; ian not in austin !-( #| A sample result of using the stepper: (define directory%001 empty) (define lookup%001 (lambda (name) ...directory%001...)) (define add-entry!%001 (lambda (name number) ...(set! directory%001 ..) ...)) (define directory%002 empty) (define lookup%002 (lambda (name) ...directory%001...)) (define add-entry!%002 (lambda (name number) ...(set! directory%001 ..) ...)) ... (define campus-phonebook (lambda (msg args) (cond [...lookup%001...add-entry!%001...]))) (define houston-phonebook (lambda (msg args) (cond [...lookup%002...add-entry!%002...]))) ; Thus we see that different phonebooks are ; using slightly different internal functions like "lookup" -- namely, ; functions which refer to different (renamed) directories! |# ;;;;;;;; Phonebooks v. 3 (with an id-number) ;; new-phonebook: --> phonebook-object ;; ... ;; (define new-phonebook (local [(define ids-so-far 0)] ; Now the body of the local, which defines new-phonebook: ; a function which increment ids-so-far, and returns a phonebook object. (lambda () (begin (set! ids-so-far (add1 ids-so-far)) ; Now, the new phonebook function(object), in the body of this local: (local [(define directory empty) (define (lookup name) (assoc name directory)) (define id-num ids-so-far) (define (add-entry! name number) (set! directory (cons (list name number) directory)))] ; Now, as the body of the local, return the dispatch function: (lambda (msg args) (cond [(symbol=? msg 'do-lookup) (lookup (first args))] [(symbol=? msg 'add-new-entry) (add-entry! (first args) (second args))] [(symbol=? msg 'get-id-num) id-num] [else (error 'phonebook-send (format "~s not a phonebook method (~s).~n" msg args))]))))))) (define campus-phonebook (new-phonebook)) (define houston-phonebook (new-phonebook)) (define austin-phonebook (new-phonebook)) (send campus-phonebook 'get-id-num empty) = 1 (send houston-phonebook 'get-id-num empty) = 2 (send (new-phonebook) 'get-id-num empty) = 4 (send (new-phonebook) 'get-id-num empty) = 5 (send campus-phonebook 'get-id-num empty) = 1 ;;;;; Exercise: ; The idea was to have shopper-card objects (one per customer), ; which keeps track of their most recently bought item. ; The following code fails to do this. ; (define make-shopper-card (local [(define most-recently-bought #f)] (lambda (name) (lambda (command some-item) (cond [(eq? command 'show-name) name] [(eq? command 'buy) (set! most-recently-bought some-item)] [(eq? command 'show-fave) most-recently-bought])))))