![]()
THE OSCARS Whose grammar grammerized the world. Whose babble babbled the best? Who will win fame, fortune, movie tickets and ice cream?
Stick around and see!
In languages like C, we think in terms of variables and
assignment.
In such a language we might compute factorial as shown
below. In this code the values associated with the variables "i" and
"a" change during the execution of the function.
int factorial(int n){ The evolution of "a" in factorial 3 looks like
int a=1; a = 1
for(i=1, i++, i>n) { a = 1 * 1 = 1
a = a*i; a = 1 * 2 = 2
} a = 2 * 3 = 6
return a; return 6
}
In programming with Scheme, we never used assignment. When we associated a name with a value, that value remained unchanged. So how did things ever change? We propagated state change through arguments to functions.
(define (factorial n i accum) The evolution looks like
(if (> i n) (fact 3 1 1)
accum (fact 3 2 1)
(factorial n (+ i 1) (* accum i)) (fact 3 3 2)
)) (fact 4 4 6)
6
The two versions of factorial are not so dissimilar. Do we really need assignment? What is the benefit of having assignment? What is the cost? Lets see by adding assigment to Scheme.
Sometimes it makes sense to think of a name as having an identity that is separate from its value.
For example, your house is your house even though you may replace a roof or a window. It makes more sense to think of a house as being more than the listing of its parts -- which may very well change over time. Similarly your student record means something that is unrelated to the exact values within it. It is still your record after you add a course, and it is still different from someone else's record who might have taken the same classes. This idea, that a name has meaning even though the associated value changes is called mutable data
> (define jean-huid 10) > (define jean-courses '(CS181 CS124 CS51 CS50)) > jean-huid 10 > jean-courses (CS181 CS124 CS51 CS50) > (set! jean-huid 11) > (set! jean-courses (cons CS182 jean-courses)) > jean-huid 11 > jean-courses (CS182 CS181 CS124 CS51 CS50)
In Scheme we can change the values of data using mutation operators like set! We can also change the values of cons cells using set-car! and set-cdr!. In this class however we do not accept the use of any mutation operators in Scheme - beware!
Without mutation, the only way to extend the course listing would be to create a new list, i.e. a new value. But the name jean-courses would always point to the original (and inaccurate) value. In other words jean-courses was immutable. We would instead rely on higher levels of the program to maintain a sense of the state of world (think about how you colored graphs and kept the current coloring).
Thinking of jean-courses as having its own identity seems very natural and is useful for that reason alone. However introducing mutable data comes with some thorny issues.
Given our notion of mutable data, we now have to tackle some more subtle issues. Lets look at some examples of these issues.
Mutable Data: Reference or Value?
CASE 1 > (define jean-courses1 '(CS182 CS181 CS51 CS50) > (define jeff-courses1 '(CS182 CS181 CS51 CS50) CASE 2 > (define jean-courses2 '(CS182 CS181 CS51 CS50) > (define jeff-courses2 jean-courses2)
In our original version of Scheme, these two cases are exactly the same. In fact we cannot distinguish whether jeff-courses points to the exact same bits in memory or not. It could and that would be fine. It does not matter.
However with mutation, these two cases are very different.
CASE 1 > (set-car! jean-courses1 'CS141) > jean-courses1 (CS141 CS181 CS51 CS50) > jeff-courses1 (CS182 CS181 CS51 CS50) CASE 2 > (set-car! jean-courses2 'CS141) > jean-courses2 (CS141 CS181 CS51 CS50) > jeff-courses2 (CS141 CS181 CS51 CS50) <--- changed! (side effect)
Clearly in this case we probably did not mean for jeff's classes to change. If we want to reuse jean's course list in creating hoaqi's, we need to make sure that we copy the value rather than the identity. Here one way to do that.
(define (copy-list lst)
(if (null? lst)
'()
(cons (car lst) (copy-list (cdr lst))))
> (define jean-courses3 '(CS182 CS181 CS51 CS50))
> (define jeff-courses3 (copy-list jean-courses3))
> jean-courses3
(CS141 CS181 CS51 CS50)
> jeff-courses3
(CS182 CS181 CS51 CS50) <---- didn't change (phew!)
However suppose now that the course lists contain something more complicated that just a symbol. Maybe the list contains another pair.
(define jean-courses4 (list (cons 'CS182 'P) 'CS181 'CS51 'CS50)) (define jeff-courses4 (copy-list jean-courses4)) jeff-courses4 > ((CS182 . P) CS181 CS51 CS50) (set-car! (car jean-courses4) 'CS141) jeff-courses4 > ((CS141 . P) CS181 CS51 CS50) <--- it changed again!!!
Clearly it wasn't sufficient to copy the list. Instead we have to
explicitly copy every element if that element is mutable
data. Having jean's courselist contain structured data is in fact
quite a reasonable assumption. If we intend to replicate it, we have
no choice but to copy every piece in the chance that it may get
mutated. Before mutation, the distinction between copying or not did
not exist.
Equality gets Complicated
Suppose we turn this around: you are given Jean's courselist and Jeff's courselist (or two other arbitrary pieces of data). Are they the same? That's an interesting question: are they the same if Jean and Jeff took the same classes? Or are they same as in, if Jean takes new course then Jeff gets it by osmosis?
> (define jean-courses1 '(CS182 CS181 CS51 CS50)) > (define jeff-courses1 '(CS182 CS181 CS51 CS50)) > (define jean-courses2 '(CS182 CS181 CS51 CS50)) > (define jeff-courses2 jean-courses2) ;; EQUAL? vs EQ? ;;--------------- (equal? jean-courses1 jeff-courses1) TRUE (equal? jean-courses2 jeff-courses2) TRUE (eq? jean-courses1 jeff-courses1) FALSE (eq? jean-courses2 jeff-courses2) TRUE
The primitive "eq?" tests if two objects have the same value AND if mutating one causes the other to change.
Of course it is possible to create a new courselist that shares part of its structure with another (e.g. (define test (cons 'CS181 jean-courses)) ) and is therefore neither equal? nor eq? but mutating test might still change jean-courses. Its complicated. Believe me.
Order Matters!
Lets take another look at factorial and write it two ways.
(define (factorial n i accum)
(if (> i n)
accum
(factorial n (+ i 1) (* accum i))
))
(define (factorial n) <--- this is similar to the C version
(let ((i 1) (a 1)) a = 1;
(define (for-loop) for(i=1; i++; i< n)
(if (> i n)
a
(begin (set! a (* a i)) a = a*i;
(set! i (+ i 1))
(for-loop))))
(for-loop)
))
In the second example, does it matter whether we set "a" or "i" first? Yes. This is because the value that "a" takes depends on "i". Notice that our C code had this exact same ambiguity - we need to know whether the increment statement (i++) in a for loop happen at the end of the body or the beginning. We also need to know whether the test case gets chacked before or after the increment.
In the non-mutable version of factorial we did not worry about the order in which (+ i 1) and (* accum i) were evaluated. This is because the value of "i" can not change (within this same scope). Instead the next function call will have a different value of "i" which is immutable within its scope. Therefore order of evaluation does not matter.
Once we use mutable data we must always worry about unintentional side effects caused by incorrect ordering.
|
Summary: Pitfalls of having Mutable Data
These complexities are not specific to Scheme, rather they occur in any language that has mutable data. Questions of what it means to be "the same", managing the intentional and unintentional sharing of state, and unintended side-effects are all part of the parcel. |
Clearly, the preponderance of languages that provide assignment and mutable data, argues that there must also be some benefits. One specific benefit is the ability to easily create "shared" memory, between different functions, that persists beyond the exceution of any particular function. For example, Jeff's course list may be changed by a registrar executing add-course, but the next operation must see the new courselist and not the old. Without mutable data we are forced to manage this transfer of state explicitly.
Consider the function "random".
(random 20) 5 (random 20) 12
In our immutable version of scheme it is not possible to create functions that behave like this. If all data is immutable, anything random depends on would still have the same value - so how could it return a different result when called with the same argument? Unless it has "memory".
Let us assume that we have a procedure rand-update that has the property that if we start with a given number x1 and call
(define x2 (rand-update x1)) (define x3 (rand-update x2))
then the sequence of values x1, x2, x3, ..., will have the desired statistical properties. Now we write rand as follows.
(define rand
(let ((x random-init))
(lambda ()
(set! x (rand-update x))
x)))
The value for "x" changes over time, and because of that rand (or the call to rand-update) returns different values each time.
(define (monte-carlo-iter trials experiment counter trials-passed)
(cond ((= counter 0) (/ trials-passed trials))
((experiment)
(monte-carlo trials experiment (- counter 1) (+ trials-passed 1)))
(else
(monte-carlo trials experiment (- counter 1) trials-passed))))
(define (monte-carlo trials experiment)
(monte-carlo-iter trials experiment trials 0))
(define (an-experiment)
(do-something (rand)))
If we did not have mutation, we would have to write the same program using rand-update directly.
(define (monte-carlo-iter trials experiment counter trials-passed x)
(cond ((= counter 0) (/ trials-passed trials))
((experiment x)
(monte-carlo trials experiment (- counter 1) (+ trials-passed 1) (rand-update x)))
(else
(monte-carlo trials experiment (- counter 1) trials-passed (rand-update x)))))
This is far less satisfying, since we have to interleave rand-update into the monte-carlo function, even though it is really the experiment that is probablistic. And if the experiment needs to generate more than one random number, this is insufficient.
There are several benefits to having mutable data: Some objects are naturally thought of as mutable. In other cases mutation can be used to create more space-efficient data structures or memorize previous computations. Mutation can be used to easily model behavior that changes over time and persistent state.
|
Mutable Data is a double-edged sword
|
Scheme is probably very different from languages that you have programmed in before taking CS 51. Here are a few interesting features.
As we will see down the road, the "simplicity" of some of these abstractions come by having some powerful programs under the ABSTRACTION BARRIER. This is not surprising though, since we've seen examples in lecture and psets where providing a programmer with a conceptually simple abstraction (dictionaries, graphs, streams) requires building some serious infrastructure (binary trees, tagged data, delayed evaluation). And language is just another abstraction.
Scheme contrasts quite sharply with C, another fairly simple language with a very different model of execution.
But Scheme and C are not the only languages you've seen (or will see in the future):
Modern languages do not fit a single mold. Rather they pick and choose properties and "specialize" in different abstractions.
my $start_marker = "Begin My Text";
my $end_marker = "End My Text";
my $remove = 0;
if ($line =~ /^$start_marker/) {
$remove = 1;
} elsif ($line =~ /^$end_marker/) {
$remove = 0;
}
def factorial(x):
if x == 0:
return 1
else:
return x * factorial(x-1)
Why are there so many languages?
Why learn a new language?
Why design a new language?
FACT: There are many good languages
COROLLARY: There is no "perfect" language
|
As we confront increasingly complex problems, no fixed programming language is sufficient for all needs. We must constantly turn to new languages in order to express our ideas more efficiently. Establishing new languages (Metalinguistic Abstraction) is a powerful strategy for controlling complexity in any engineering discipline. Its is especially important in computer science because not only can we create new languages, we can implement them too. |
Lets briefly take a look at an evaluator for a scheme-like language, called "MinScheme", which is even more minimal than Scheme. All we have is:
Of course we can use lambda to build complex things, new procedures, cons cells, list, etc. A program that interpretes MinScheme must implement the MANTRAs we have already internalized.
;;----------------------
;; THE EVAL/APPLY LOOP
;;----------------------
(define (minscheme-eval exp env)
(cond ;; SELF-EVALUATING
((number? exp) exp)
;; NAMES
((variable? exp) (lookup-name exp env))
;; SPECIAL FORMS
((define? exp) (eval-define exp env))
((if? exp) (eval-if exp env))
;; LAMBDA
((lambda? exp) (eval-lambda exp env))
;; COMBINATION
;; (procedure arg1 arg2 arg3 ..)
((combination? exp)
(minscheme-apply
(minscheme-eval (exp-procedure exp) env) ; evaluate the operator
(map (lambda (a) (minscheme-eval a env)) ; evaluate the arguments
(exp-arguments exp)))) ; then call minscheme-apply
;; OTHERWISE, don't recognize this type of statement
(else (display-error "Unrecognized expression --- EVAL" exp))
))
;; Minscheme-apply
;;------------------
(define (minscheme-apply procedure arguments)
(cond ;; PRIMITIVE PROCEDURE like PLUS
((primitive-procedure? procedure)
(apply-primitive-procedure procedure arguments))
;; PROCEDURE CREATED BY LAMBDA
((compound-procedure? procedure)
(minscheme-eval
(procedure-body procedure)
(extend-environment (procedure-parameters procedure)
arguments
(procedure-env procedure))))
(else (display-error "Unknown procedure type --- APPLY" procedure))
))
;; To Run
;;---------
(minscheme-eval '(+ 2 3) global-env)
(minscheme-eval '(define square (lambda (x) (* x x))) global-env)
(minscheme-eval '(square 5) global-env)
This eval/apply loop is the heart of a Scheme
interpreter. Notice that the style looks like dispatch on type, where
type is determined by the keyword in the first position of the
expression (e.g. define? simply checks if the first symbol in the
expression is 'define). An interesting observation is that a Scheme
interpreter program is not really that complicated. In fact it is
simpler than many programs it can run.
Cool Programs written in Scheme/LISP
|