| 11/14/08 | Lecture 31. Efficiency of algorithms: sorting.
Insertion sort: worst case running time is Theta(n^2). Merge sort: worst case running time is O(n log n). |
|---|---|
| 11/12/08 | Lecture 31. Efficiency of algorithms: table lookup.
Linear search of a list: worst case running time is Theta(n). Binary search of a sorted list: worst case running time is Theta(log n). Lookup in a hash table: expected time O(1) (with suitable assumptions about the hash function.) Binary search trees: worst case running time is O(depth of the tree). |
| 11/10/08 | Lecture 30. Compilers: code generation. Efficiency of algorithms.
We looked at the task of generating TC-201 assembly language code from parse trees of the toy language we considered in the last lecture, Tiny Higher Level Language. Please see the notes: Code generation. We also considered the time required to search our ``table'' data structure (represented as a Scheme list of lists, each list containing a key and a value) to implement table lookup. We can consider the worst case time (the most it could ever take), or the best case time (the least it could ever take), or an average case time (which requires that we choose some probability distribution over the possible inputs to the algorithm.) If we can give a good upper bound on the worst case time, that also bounds the best case time and the average case time. However, a lower bound on the worst case time may not be so informative, because it may not represent the ``typical'' behavior of the algorithm in applications of interest to us. In our case, the worst case time is proportional to n, the number of element in the table to be searched, because we may have to search until the last element in the table. In terms of big-Oh, big-Omega and big-Theta notation, we have that the worst case time of our algorithm to search for a key is both O(n) and Omega(n) and therefore also Theta(n). We'll continue with the topic of algorithm efficiency in the next lecture. |
| 11/7/08 | Lecture 29. Compilers: lexical and syntactic analysis.
We first considered three decision problems: (0) Given a Turing machine M and a string w, does M halt with input w?, (1) Given a deterministic finite acceptor M and a string w, does M accept w?, and (2) Given a context free grammar G and a string w, is w in L(G)? Problem (0) is the notorious Halting Problem, and is algorithmically unsolvable, as we saw earlier in the term. For problem (1), we can imagine an algorithm that starts at the start state and makes moves according to the transition function and the successive characters of w until all of the characters have been processed -- at that point, the algorithm checks whether the state it has reached is an accepting state or not, and answers accordingly. Thus, problem (1) is (efficiently) algorithmically solvable. (It is interesting to note that if instead of a deterministic finite state acceptor, we are given a regular expression E and a string w and must determine whether w is in L(E), the problem is still algorithmically solvable, but apparently not efficiently so.) It is not so easy to find an algorithm for problem (2), but it too is efficiently algorithmically solvable. We looked at a BNF grammar for a small part of the Pascal programming language, as part of understanding what a compiler does. (Please see the handout: Tiny Higher Level Language.) The job of a compiler is to take a program in a higher level language (eg, Java or C++) and translate it into an equivalent (in some sense) machine language program that can be executed by the computer the program is supposed to run on. The first phase of a compiler is typically lexical analysis -- translating a raw sequence of characters into a sequence of ``tokens'' -- meaningful units of the program, for example, identifiers, numbers, and operations. The second phase is syntactic analysis -- the sequence of tokens is parsed according to the grammar of the language, and a parse tree is produced. This parse tree is the basis for code generation, which we will consider in the next lecture. |
| 11/5/08 | Lecture 28. Context-free grammars and languages.
Please see the notes: Context free languages. In addition, we looked at a one-page handout giving a portion of the BNF definition of the computer language Pascal. |
| 11/3/08 | Lecture 27. Deterministic finite acceptors and Regular languages.
Please see the notes: dfas. Last lecture we saw how regular expressions can be used to denote sets of strings. In this lecture, we see another method of specifying sets of strings: deterministic finite acceptors. As an example, we considered a dfa with 2 states to accept the set of all strings of a's and b's with an odd number of a's. Deterministic finite acceptors and regular expressions denote exactly the same sets of strings, the regular languages. To illustrate this fact, we constructed a regular expression to denote the set of all strings of a's and b's with an odd number of a's, and a dfa to accept L(c(a|d)(a|d)*r). We also saw that no dfa can accept the set of all strings of a's anb b's that are palindromes, that is, palindromes are not a regular set. |
| 10/31/08 | Lecture 26. Regular expressions.
Please see the notes: Regular expressions. |
| 10/29/08 | Lecture 25. More objects: stacks and queues.
We looked at an implementation of a stack data structure in the style of Scheme objects. A stack can be described as a last-in, first-out (LIFO) data structure. Most of the discussion may be found in the notes: A stack object. However, the implementation we came up with in class is slightly different, because we chose to have no initial data for a stack -- a stack will always be created as empty in this implementation. This necessitated an extra "let" in the code for make-stack, as follows:
(define make-stack
(lambda ()
(let ((stack '()))
(lambda (command . args)
(cond
((equal? command 'empty?) (null? stack))
((equal? command 'top) (car stack))
((equal? command 'pop!) (set! stack (cdr stack)))
((equal? command 'push!) (set! stack (cons (car args) stack)))
(else 'error))))))
The let creates a local environment with the variable stack
assigned the initial value of the empty list.
This happens once for each call to make-stack, so that each created
stack has its own local environment with the variable stack in it.
We also discussed the use of Scheme's case syntax to make
this program easier to read.
A queue is a first-in, first-out (FIFO) data structure, analogous to the British usage of "queue" for a line of people (for example, waiting to purchase movie tickets.) New elements join the line at the "back", and elements are selected from the "front" to be served (and then exit the line). We could just use a list to represent the data in a queue, but then accessing the "back" of the queue to add new elements would take time proportional to the number of elements in the queue, which would not be constant-time. Instead, we choose a structure that is basically a list, with the addition of a pointer to its last element. For the precise data structure, a box-and-pointer picture of it, and an implementation of operations to manipulate it, please see Section 2.9 of the textbook. |
| 10/27/08 | Lecture 24. Mutators, environments, objects.
In the previous lecture we were introduced to the Scheme mutators set!, set-car! and set-cdr!. In this lecture, we see how to use Scheme procedures to implement objects; to do this, we need a little more information about how environments and procedures interact. Please also see the notes: Scheme environments. When a procedure is created (by evaluating a lambda expression in some environment), the following information is recorded about it: (1) the list of formal arguments, (2) the Scheme expression(s) that form the body of the procedure, and (3) the "birth environment" of the procedure, that is, the environment in which the lambda expression that creates this procedure is evaluated. When a procedure is applied to actual arguments, the following operations take place: a new local environment is created with the formal arguments of the procedure bound to the actual values of the arguments, a *search pointer* is created for this new local environment that points back to the birth environment of the procedure (which was recorded when the procedure was created), and, finally, the expression(s) forming the body of the procedure are evaluated in this new environment, and the resulting value is returned as the value of the procedure. What is new in this description is the fact that the "birth environment" is recorded for a procedure, and that when the procedure is applied, its birth environment is where the local environment of formal and actual arguments points to continue a search for symbols. Class members contributed their ideas about what programming with objects involves. An object is some data and some procedures to operate on it (methods), packaged together. A class is a general blueprint for objects of a given kind, and can be used to create several different objects of this kind. Objects often involve a certain aspect of "data hiding" -- making sure that the data in an object can only be manipulated by using the specified methods for the object. They may also involve a certain amount of data sharing, for example, via global variables. Different classes are organized in part by inheritance -- you may choose create a new class from an old class by adding new methods, or overriding old methods. Useful classes are organized into libraries, in which objects (almost) of the kind you need may already be defined. We develop a very simple kind of object, a counter. Suppose we create a counter with some initial value (an integer) and operate on it using two methods: (1) to get the current value of the counter, and (2) to increment the counter. First, without an object-oriented approach, we could just create a global variable count, initialized to the desired value, and then have two procedures to return the value of the counter and to increment it, as follows. > (define count 0) > (define check-count (lambda () count)) > (define inc-count (lambda () (set! count (+ 1 count))))Evaluated at the top-level, these expressions create a variable named count, initialize it to 0, and create two procedures, one to get the value of count and one to increment count. Thus, we have: > (check-count) 0 > (inc-count) 0 > (check-count) 1 > (inc-count) 1 > (check-count) 2However, this leaves the data, count, vulnerable to being modified by other procedures, since it is accessible as a global variable. (In particular, a separate procedure to get its value seems like overkill, because we can just evaluate count to get its value.) Another problem is that if we want two counters, or three, we have to repeat these definitions as many times as we want them. We can use Scheme procedures and environments to get more object-like behavior, as follows.
(define make-counter
(lambda (count)
(lambda (command)
(cond
((equal? command 'value) count)
((equal? command 'increment) (set! count (+1 count))
count)
(else 'error)))))
This procedure can be used to create procedures that function as
counter objects, as follows.
> (define counter1 (make-counter 0)) > (counter1 'increment) 1 > (counter1 'increment) 2 > (define counter2 (make-counter 17)) > (counter2 'increment) 18 > (counter1 'increment) 3 > (counter2 'increment) 19In this example, we have used counter-maker to create two independent counters, counter1, (initialized to 0 and incremented three times) and counter2 (initialized to 17 and incremented twice). Please also see the notes: Scheme environments. |
| 10/24/08 | Lecture 23. Scheme constructs in assembly-language, mutators.
To finish the discussion from last lecture about implementing Scheme constructs in TC-201 machine language: we said that a Scheme number or pointer or empty list would be represented by 2 contiguous memory registers, the first giving the type (0 for a number, 1 for a pointer, -1 for the empty list) and the second giving the value (the number, or the machine address, or 0 for the empty list). A cons cell is then represented as 4 consecutive memory registers, containing two such values (the car and the cdr). We illustrated how the list lst => (13 27) might be represented in memory, and what would happen when we evaluated (define new-lst (cons 6 lst)), and also (define new-lst (list 6 lst)). The Scheme basic operations of cons, car, cdr and null? are constant-time operations in this implementation. That is, there is some absoluted constant K such that the number of machine language instructions to execute a cons or a car or a cdr or a null? operation is no larger than K, no matter how how many elements there might be in the list being cons'ed to, or car'ed, or cdr'ed or tested for being empty. This is because the cons operation just has to locate 4 consecutive memory cells and copy its two arguments (each represented by 2 memory locations) into the first two and second two of these 4 cells. The car operation just returns the first two memory registers from the cons cell that is its argument, and the cdr operation just returns the second two memory registers from its cons cell argument. The null? test just compares the two memory registers representing its arguments with -1 and 0, respectively. Consider the procedure we wrote to append two lists:
(define our-append
(lambda (lst1 lst2)
(if (null? lst1)
lst2
(cons (car lst1) (our-append (cdr lst1) lst2)))))
In order to analyze how much time this procedure takes, let's
first consider what happens when we evaluate (our-append u v),
where u is the list (a b) and v is the list (x y).
Then at the top-level call, lst1 is a pointer to the list
(a b) and lst2 is a pointer to the list (x y).
In terms of box-and-pointer diagrams, we have:
__________ __________
u,lst1 ----->| a | ---|---->| b | () |
---------- ----------
__________ __________
v,lst2 ----->| x | ---|---->| y | () |
---------- ----------
Because lst1 is not the null list (a constant-time check), there
is a recursive call with (cdr lst1) and lst2, so after constant time
we arrive at the situation of the next level call:
__________ __________
u ----->| a | ---|---->| b | () |
---------- --> ----------
|
lst1 ------------------
__________ __________
v,lst2 ----->| x | ---|---->| y | () |
---------- ----------
Once again, lst1 is not the null list, and there is a recursive call
with (cdr lst1) and lst2, so after constqant time we arrive
at the situation:
__________ __________
u ----->| a | ---|---->| b | () |
---------- ----------
lst1 = ()
__________ __________
ret,v,lst2 ----->| x | ---|---->| y | () |
---------- ----------
This is the base case, which just returns lst2, that is the
pointer to the list (x y) above.
The second level call cons'es the car of lst1 (which is b)
onto this list, creating a new cons cell, with left
part b and right part equal to the returned value, so we have
__________ __________
u ----->| a | ---|---->| b | () |
---------- ----------
----------
ret----->| b | ---|--
---------- |
|
v__________ __________
v,lst2 -------------->| x | ---|---->| y | () |
---------- ----------
The top level call cons'es the car of lst1 (which is a)
onto this list, creating a new cons cell with left part a
and right part equal to the returned value, so we have:
__________ __________
u ----->| a | ---|---->| b | () |
---------- ----------
__________ __________
ret ----->| a | ---|---->| b | ---|--
---------- ---------- |
|
v__________ __________
v,lst2 -------------------------------->| x | ---|---->| y | () |
---------- ----------
We see that the returned value is (a b x y), achieved by copying all
the elements of lst1, but not any of the elements of lst2.
Thus, the time for our-append is proportional to the length of lst1.
This is not constant-time.
Mutators: set!, set-car! and set-cdr!. These mutators are built-in Scheme procedures. The exclamation point (or bang) character is a Scheme convention to signal a mutator, that is, a procedure that changes some value in memory. The procedure set! allows the programmer to change the value of a variable in the current environment. For example, consider the following sequence of evaluations at the top-level of the Scheme interpreter. > (define x 0) > x 0 > (set! x (+ x 1)) > x 1The (define x 0) adds x to the top-level environment, with the value of 0. The (set! x (+ x 1)) looks up the symbol x and changes its value to be incremented by 1 in the environment where it finds x, in this case, the top-level environment. The procedure set-car! changes the car of a cons cell, and the procedure set-cdr! changes the cdr of a cons cell. Examples of the use of set! can be seen in the lecture notes for the next lecture, where it is used to implement a kind of counter object. Examples of set-car! and set-cdr! follow: > (define lst '(a b c)) > lst (a b c) > (set-car! lst 'd) > lst (d b c) > (set-car! (cdr lst) 'e) > lst (d e c) > (set-car! (cddr lst) 'f) > lst (d e f) > (define lst2 '(x y z)) > (set-cdr! (cdr lst) lst2) > lst (d x y z)This newfound power induced us to create a "circular list" in which we have 3 cons cells, with cars of a, b and c, and cdrs pointing from a to b, b to c, and c back to a. Such a list can be created as follows. > (define clst '(a b c)) > (set-cdr! (cddr clst) clst)MIT Scheme prints out screenfuls of (a b c a b c a b c ... if you attempt to examine the value of clst at this point. Other Schemes "catch" the circularity and print out a representation to indicate it to the user. (I will spare you an ASCII drawing of the structure of clst.) We can write a procedure to test whether a "list" is truly a list or has the cdr of its last cons cell pointing back to the first element, as follows.
(define circle?
(lambda (lst)
(circle-help? lst lst)))
(define circle-help?
(lambda (lst start)
(cond
((null? lst) #f)
((eq? (cdr lst) start) #t)
(else (circle-help? (cdr lst) start)))))
Here we have used a helper procedure because we need to
keep around a pointer (start) to the original first element of the
input to compare to the cdr of the last cons cell.
Then the helper procedure follows cdr pointers down the
list until either it finds a cons cell whose cdr pointer
points to the same cell as start (which means we are
in the case of a circular list) or it reaches the empty list
(which means the input was not a circular list.)
The built-in procedure eq? returns #t if its two arguments
are pointers to the same cons cell, and #f if they point
to different cons cells.
(In terms of machine language, this is the question of whether
the two arguments are the same machine address.)
For example (eq? '(a b c) '(a b c)) => #f, although
(equal? '(a b c) '(a b c)) => #t.
(This is because there are two separate groups of cons cells
created by the two instances of '(a b c).)
|
| 10/22/08 | Lecture 22. The uses of machine language.
An understanding of machine language grounds discussion of the running time and space use of programs and algorithms, and also the process of transforming a program in a higher-level language like Java into an equivalent machine language program that is executable by the hardware. Of course, real architectures are much more complex and sophisticated than the model embodied by the TC-201; to make computers run faster and faster, designers have used various techniques, for example, several levels of cache memory (memory that is more expensive and faster than the main memory of the computer, which is used to store the addresses and values the program is currently working on), pipelining (simultaneous decoding and execution of a sequence of instructions), branch prediction (trying to guess which way a test will come out), prefetching of memory (moving data that is "about to be used" into cache), speculative execution (executing instructions before you know for sure they are going to be executed), and many others. Moore's Law, which describes a kind of exponential growth in how much computing power $1.00 buys, will have to come to an end somewhere; computer architecture folks are saying the end is near, and we will have to figure out how to use many processors in parallel if we are to continue to make faster computers. However, in our simplified model we can assume that each machine instruction takes one time unit, and then the amount of wall clock time that a program runs will be directly proportional to how many machine instructions it executes before it halts. The analysis of the amount of time a program in a higher-level language like Java runs implicitly depends on how the program's steps relate to machine language instructions. As an example, we can analyze how we might implement a one-dimensional array of numbers on the TC-201. An array is a finite sequence of values that we access by an index. We'll assume zero-based indexing, so an array A of 7 values might be indexed by A[0], A[1], ..., A[6]. A typical assignment statement involving an element of an array in a higher-level language might be A[3] := A[3] + 1;The meaning of this is to add 1 to the value of array element A[3]. If we choose to represent the array by 7 contiguous memory locations, say 120 through 126, and also a pointer to the start of the array and the length of the array, we might have the following memory contents: 118: 120 this is a pointer to the first cell of the array 119: 7 this is the length of the array 120: 13 this is A[0] 121: -130 this is A[1] 122: 6 this is A[2] 123: 17 this is A[3] 124: 11 this is A[4] 125: 3 this is A[5] 126: 2 this is A[6]To figure out how to translate "A[3] := A[3] + 1" into assembly language instructions, we see that we can access the start of the array in 118, add 3 to its contents to get 123, store that in another memory location, and then load indirect (loadi) from that location to get the value of A[3]. So a simple version of the translation might look like:
load 118 loads the address of the start of the array
add three adds 3 to it
store temp stores it in a temporary location
loadi temp loads *indirectly* through temp
add one adds 1 to the value of A[3]
storei temp stores *indirectly* through temp
...
...
three: data 3
one: data 1
temp: data 0
To understand the effect of the instructions above, try simulating
them to see that the value in memory register 123 becomes 18, as desired.
What if we want array bounds checking for our array reference? An array bounds check makes sure that when we index an array with a number, that number is between 0 and the length of the array minus 1. This prevents the program from accidentally reading or storing memory registers outside of the array. In the case of "A[3] := A[3] + 1", if we have declared the array to have 7 elements, the compiler can check that 3 is between 0 and 6, and does not need to add any instructions to perform the array bounds check. However, if the instruction were "A[N] := A[N] + 1", then depending on the value of N when this instruction is executed, the index may or may not be in bounds. Without array bounds checking we would have the similar code:
load 118 loads the address of the start of the array
add n *** adds the value of n to it
store temp stores it in a temporary location
loadi temp loads *indirectly* through temp
add one adds 1 to the value of A[3]
storei temp stores *indirectly* through temp
...
...
n: data 0
one: data 1
temp: data 0
Here the code adds the value of n to the address of the
start of the array instead of 3.
For an array bounds check, before we access A[N], we check
whether N is between 0 and the length of the array, as follows:
load 119 loads the length of the array
sub n subtracts n from it
skippos skip on positive (means length > n)
jump error (n >= length, not in bounds)
load n
add one
skippos skip on positive (n >= 0)
jump error (n < 0)
... at this point, continue with the access (above)
It is clear why we'd like the compiler to be very smart about
statically doing array bounds checks, so we do not end up with
extra instructions executed for every array reference.
However, in any case, the number of machine instructions that have
to be executed to access one element of an array is clearly a CONSTANT,
which justifies considering an instruction like "A[n] := A[n] + 1" to
be a constant-time operation in a higher-level language.
We now consider the problem of representing a list like the Scheme list (13 27) at the level of TC-201 instructions. The list (13 27) can be thought of as consisting of two cons cells, the first one with a car of 13 and a cdr pointing to the second one, and the second one having a car of 27 and a cdr of (). If we take the idea of "run-time types" from Scheme, we can allocate two consecutive memory registers to hold the type and value information for a number, a pointer, or the empty list. If the first register contains 0 then the type is a number, and the second register contains a number (in TC-201 sign/magnitude representation). If the first register contains a 1 then the type is a pointer, and the (low-order 12 bits of) the second register contains the memory address of the value pointed to. Finally, if the first register contains a -1, then the type is the empty list, and the second register just contains 0. In this representation we have:
131: 0 these two registers give type = number
132: 13 value = 13
...
153: 1 these two registers give type = pointer
154: 251 adress = 251
...
159: -1 these two registers give type = empty list
160: 0
Then a cons cell will consist of a pair of typed values like this,
taking up 4 consecutive memory locations.
For example, the list (13 27) might be represented by:
170: 0
171: 13
172: 1
173: 252
...
252: 0
253: 27
254: -1
255: 0
This gives us two cons cells, not contiguous in memory, where
the first one has a car of 13 and a cdr that is a pointer to
the list (27), which is represented by the second cons cell,
whose car is 27 and whose cdr is ().
To refer to the list (13 27) we need a value of type pointer whose
address portion is 170.
|
| 10/20/08 | Lecture 21. Arithmetic, assembler, loadi and storei.
Please also see the specification tc-201.txt. With 16 bits, we can represent at most 2^(16) = 65536 integers. We'd like to have about half positive and half negative. Because the set of integers we'd like to represent (eg, -3, -2, -1, 0, 1, 2, 3) has an odd number of elements, and the number of available patterns is even, there will be some "kink" in the representation. The TC-201 computer uses sign and magnitude representation. The leftmost bit is the sign bit (0 for +, 1 for -) and the remaining 15 bits give the magnitude (or absolute value) of the number. The "kink" in this system is two representations of zero: +0 and -0, represented by (respectively) 16 0's, or a 1 bit followed by 15 0's. Other popular systems of representation are two's complement and one's complement. Two's complement arithmetic is based on arithmetic modulo 2^b, where b is the number of bits available. For example, for b = 4, arithmetic modulo 16 gives (9 + 9) = 2 (mod 16). If we look at this in binary, adding 1001 and 1001 gives 10010, which is 2 if we drop the high order 1. Thus, arithmetic mod 16 can be done by our 4-bit binary addition circuit, where we ignore the carry out of the last column. In this system we take the negative of a number like 4 to be the number that we add to it to get 0 mod 16, that is, 12 is the negative of 4 because (4 + 12) = 0 (mod 16). An algorithm to find the negative of a number is to complement its bits and then add 1. This works because complementing its bits is equivalent to subtracting it from 15, and then adding 1 gives the result of subtracting it from 16. For example, to find the negative of 5 we complement the bits of 0101 to get 1010 and then add 1 to get 1011 (which is correct, because 11 is the negative of 5 mod 16.) The "kink" in two's complement is that there is a number (8 mod 16) that is its own negative -- we take it to be -8, and there is no corresponding +8. In one's complement, taking the negative of a number is easy: just complement the bits. However, arithmetic is no longer just arithmetic modulo 2^b. In one's complement, there are again two representations of zero: all 0's and all 1's. For comparison, here are the sign and magnitude, two's complement, and one's complement interpretations of numbers with 4 bits: bit pattern sign/magnitude two's complement one's complement 0000 0 0 0 0001 1 1 1 0010 2 2 2 0011 3 3 3 0100 4 4 4 0101 5 5 5 0110 6 6 6 0111 7 7 7 1000 -0 -8 -7 1001 -1 -7 -6 1010 -2 -6 -5 1011 -3 -5 -4 1100 -4 -4 -3 1101 -5 -3 -2 1110 -6 -2 -1 1111 -7 -1 -0 In regard to this week's homework, we consider the process of converting a program from assembly language to machine language. We represent a program in Scheme as a list of lists, each list representing an instruction or a data statement. For example, in the homework prog1 is the list ((start: load one) (store count) (halt) (one: data 1) (count: data 0))Labels are symbols ending in a colon -- see the homework for how to convert a symbol to a string and vice versa, and how to represent the colon character. To assemble this program, we proceed in two passes. In the first pass, we build a symbol table consisting of all the labels in the program and the addresses that they represent. To know what addresses the labels will translate to, we provide the additional argument of what address the first instruction is to be loaded to. For example, if we load the first instruction into address 5, then the successive instructions and data statements translate to 16-bit patterns that occupy memory registers 5,6,7,8,9 in the case of prog1. Thus, we create the symbol table ((start: 5) (one: 8) (count: 9)), giving the translations of these labels. In the second pass of the assembly process, each instruction or data statement is translated into a 16-bit pattern. For example, to translate the first instruction: (start: load one), we ignore the label, look up the symbol load in the table provided in the homework to find that it is the bit pattern (0 0 0 1), and then look up the symbol one (here we will have to add a colon to the end of the symbol) in the symbol table, find that it is address 8, and then translate the address 8 into a 12-bit pattern: (0 0 0 0 0 0 0 0 1 0 0 0). Appending these two bit patterns gives the translation of the first instruction: (0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0). Last Wednesday we saw one method of storing a sequence of numbers read in from the user into a sequence of memory locations, but it involved "self-modifying" code -- we added 1 to the store instruction to make it store into the next location. To avoid this, we introduce two new instructions into the TC-201: the "load indirect" (loadi) and "store indirect" (storei) instructions. To execute the loadi instruction, we extract the address field of the instruction, say memory address A, and then take the low order 12 bits of the contents of memory register A as an address, say B, and then load the contents of memory register B into the accumulator. Similarly, for the storei instruction, we extract the address field of the instruction, say memory address A, and then take the low order 12 bits of the contents of memory register ! as an address, say B, and the copy the contents of the accumulator into memory address B. For examples of loadi and storei, see tc-201.txt. As an example, we write a program to read in a zero-terminated sequence of numbers from the user and store them in consecutive memory locations (starting with the location table) as follows.
read-next: read number
load number
skipzero
jump store-it
halt
store-it: storei pointer
load pointer
add one
store pointer
jump read-next
number: data 0
pointer: data table
one: data 1
table: data 0
In this example, the first number will be stored in the memory
location labeled by the label table:, the next number will be
stored in the next memory location (because pointer will have been
incremented by 1), and so on, until the first time 0 is read
in (when the program just halts.)
Thus we can use loadi and storei to avoid the use of "self-modifying"
code.
For the implementation of read and write (in homework #4) you'll need a way to read in a number from the user and print out a number for the user. To print out a message, you can use display, for example, (display "hi ") displays the string "hi" and (display number) displays the current value of number. To output a carriage return and line feed, evaluate (newline). To read in a Scheme expression (in particular, a number), you evaluate (read). This causes the program to wait for the user to type in a number and returns its value as the value of the expression (read). Thus, for example, to prompt the user for a number and to print out the number entered by the user, you can use something like: (let ((x (begin (display "type a number = ") (read)))) (display "the number you typed = ") (display x) (newline)) |
| 10/17/08 | In lieu of Lecture 20: Exam 1. |
| 10/15/08 | Lecture 19. The TC-201 Computer, continued.
Please also see the notes: TC-201 Programs. And also see the specification tc-201.txt. |
| 10/13/08 | Lecture 18. The TC-201 Computer.
Please also see the notes: The TC-201 Computer. |
| 10/10/08 | Lecture 17. Guest lecture on Computer Graphics by Prof. Rushmeier. |
| 10/8/08 | Lecture 16. Random access memory.
Please also see the notes: Random-access memory. |
| 10/6/08 | Lecture 15. Sequential circuits and memory.
Please also see the notes: Memory. We examined the behavior of a NAND latch: two NAND gates whose outputs are fed back to be an input of the other NAND gate, and discussed how its output depends on the history of the inputs to the circuit, and how this property can be used to store one bit (1 or 0) of information. We looked at a more complicated circuit, the D-latch, constructed of 4 NAND gates, with a NAND latch at its heart. The D-latch has two inputs, D (for Direct set) and E (for enable) and two outputs, Q and Q' (which are always the complements of each other.) The behavior of the D-latch is that when E = 0, Q and Q' just remain in their previous state, and when E = 1, the value of Q becomes the value of the input D and the value of Q' becomes the complement of the value of Q. Several such 1-bit memories can be combined using a common E input to make a multi-bit register. Several registers can be combined to give a random-access memory. We considered a design with 4 register, each with 4 bits, and a Memory Address Register (MAR) of two bits, and designed circuitry to use the address in the MAR to select the outputs of one of the registers to appear on the output lines of the memory. We also discussed how a similar selection could be used to write values from a set of input lines to the memory into a selected one of the memory registers. |
| 10/3/08 | Lecture 14. Some advice for the homework,
Boolean bases, NAND, NOR, and a non-combinational circuit.
Please also see the notes: Boolean bases. Perlis aphorisms: 21. Optimization hinders evolution. 10. Get into a rut early: Do the same process the same way. Accumulate idioms. Standardize. The only difference(!) between Shakespeare and you was the size of his idiom list -- not the size of his vocabulary. Some advice for homework: if you find the code for a single procedure growing to a page, try to figure out how to factor it into subprocedures which can be independently written and tested (and commented.) For example, rather than trying to deal with a whole Turing machine configuration, you might write a subprocedure to deal with just its tape, and use that in your procedure to deal with configurations. As an example, there is a built-in library procedure list-ref that returns an element of a list given an index for it: (list-ref '(a b c d) 2) => c. Even if you did not know of the existence of this procedure, you might have thought to write it yourself (it is not very hard.) More advice for the homework: in debugging your code, you may want to print out the value of some expression from a procedure. For that, (display exp) prints out the value of exp when it is evaluated, and returns an "unspecified value." The argumentless procedure (newline) prints out a newline (carriage return and line feed) to the terminal. So, to see the value of an expression printed as well as returning its value, you could define a procedure (define print (lambda (x) (display x) (newline) x)). Be sure that displays and newlines are commented out from your code when you submit it, or it will not test properly against the test cases. In connection with display, the special form (begin exp1 exp2 ... expn) evaluates the expressions in order: exp1, then exp2, ..., and finally expn, and returns the value of the last one, expn. Still more advice for homework: review let and let* -- they can help you unclutter your code. The built-in procedure map can be useful -- it can be used to process the elements of 2 (or more) lists in parallel, making a list of the results. For example, if we define
(define zip
(lambda (lst1 lst2)
(map (lambda (x y) (list x y))
lst1
lst2)))
then (zip '(1 2 3) '(4 5 6)) => ((1 4) (2 5) (3 6)).
The following (not built-in) procedure can also be of assistance:
(define filter
(lambda (pred? lst)
(cond
((null? lst) '())
((pred? (car lst)) (cons (car lst) (filter pred? (cdr lst))))
(else (filter pred? (cdr lst))))))
As examples we have
(filter odd? '(1 3 2 4 7 5 6 8)) => (1 3 7 5) (filter (lambda (x) (> x 5)) '(1 3 2 4 7 5 6 8)) => (7 6 8)The procedure filter takes a predicate (remember, that is just a procedure that returns #t or #f) of one argument and a list, and returns a list of those elements of the input list for which the predicate is true (that is, not #f). This is useful if you want to select the elements of a list that satisfy some property. Continuing with Boolean functions and circuits, we have shown (via the sum of products algorithm) that every Boolean function can be represented using AND, OR, and NOT, which means that gates to compute 2-input AND and OR and 1-input NOT are sufficient to build combinational circuits to compute any Boolean function. Thus, {AND, OR, NOT} is a complete Boolean basis. What about just {AND, OR}? Is this a complete Boolean basis? Yes, because we can express OR in terms of AND and NOT, as follows: (x + y) = (x' * y')'. Thus we could replace every OR with this expression on AND and NOT and have an expression using just AND and NOT. Dually, the set {OR, NOT} is also a complete Boolean basis. What about the sets {AND, OR} and {XOR, NOT}? Are these complete Boolean bases? There seemed (after awhile) to be general agreement that we could not find a circuit on AND and OR (even using constants 0 and 1) that could replace NOT. The case of {XOR, NOT} was deferred until next lecture. Two more Boolean functions of two variables: NAND, defined as (x NAND y) = (x * y)' and NOR, defined as (x NOR y) = (x + y)'. These have the following truth tables: x y (x NAND y) (x NOR y) 0 0 1 1 0 1 1 0 1 0 1 0 1 1 0 0What about {NAND}? Is this a complete Boolean basis? Yes, because we can replace x' with (x NAND x):
x (x NAND x) x'
0 1 1
1 0 0
And we can replace (x + y) with ((x NAND x) NAND (y NAND y)),
so NAND is sufficient to compute both NOT and OR, which we know
is a complete Boolean basis.
(Or, we can observe that (x * y) is ((x NAND y) NAND (x NAND y)),
thereby getting both NOT and AND.)
Dually, {NOR} is also a complete Boolean basis.
A non-combinational circuit with inputs x and y and outputs q and u can be defined by q = (x NAND u) and u = (y NAND q). This is non-combinational because we have a "loop" -- q depends on u and u depends on q. This strange circuit gives us a 1-bit memory and allows us to construct a computer's random access memory. We'll look carefully at this in the next lecture. |
| 10/1/08 | Lecture 13. Circuits for equality and binary addition.
Please see the notes: circuits for equality and binary addtion. |
| 9/29/08 | Lecture 12. DNF, gates and combinational circuits.
We showed (by example) that any Boolean function can be represented by a Boolean expression using AND, OR and NOT, in Disjunctive Normal Form (DNF) or in Conjunctive Normal Form (CNF). We introduced gates for AND, OR, and NOT and used them to design a circuit for exclusive or (XOR). We also used them to design a circuit to compute an alarm signal s from the Boolean inputs k = key in ignition, d = door closed, and b = seatbelt fastened. Please see the notes: DNF, gates and combinational circuits. |
| 9/26/08 | Lecture 11. Boolean functions and expressions.
Some items related to homework #2 follow. Two problems deal with nonnegative integers in ternary (base 3). The integers zero through ten in ternary are 0, 1, 2, 10, 11, 12, 20, 21, 22, 100, 101. The rightmost position is the 1's place, then the 3's place, then the 9's place, then the 27's place, and so on. Thus 211 base three is two nines, one three and one ones, for 18+3+1 = 22 base ten. The special form let* may be useful. This is "syntactic sugar" for nested lets. That is, (let* ((a 1) (b (+ a 1))) (+ a b)) functions as though we had a nested let for each successive variable in the let list, that is: (let ((a 1)) (let ((b (+ a 1))) (+ a b))). Both expressions return the value 3. In homework #2 you will be constructing a simulator that simulates Turing machines using Scheme. This shows that Scheme (in an idealized machine with no memory limitations) is Turing-complete, that is, capable of simulating a given Turing machine on a given input. For the next portion of the course, we'll look at Boolean functions and expressions as a means of describing and designing the circuits that make up the hardware of a (simplified model of a) computer. Please see the notes: Boolean functions and expressions. |
| 9/24/08 | Lecture 10. The Unsolvability of the Halting Problem.
The Halting Problem: given a program P and an input x, does P halt on x? We prove that the Halting Problem is unsolvable, that is, there is no program that on inputs P and x outputs #t if P halts on input x and #f if P does not halt on input x. The proof in lecture was given in terms of Scheme programs. That is, we prove that there is no Scheme procedure halts? that satisfies the following specification: (halts? exp1 exp2) returns #t if exp1 is a procedure that halts with input exp2, and returns #f otherwise. The proof is by contradiction: assume that there is a procedure halts? that satisfies the specification in the preceding paragraph. Then we use halts? to write another procedure (contrary exp) as follows.
(define contrary
(lambda (exp)
(if (halts? exp exp)
(infinite-loop)
'hi)))
Note that infinite-loop is the procedure we wrote several lectures
ago that has no arguments and recursively calls itself, and never
returns a value.
That is,
(define infinite-loop (lambda () (infinite-loop))) Now that we have defined contrary, we ask whether (contrary contrary) halts (i.e., returns a value). We consider the two cases: (1) (contrary contrary) halts, and (2) (contrary contrary) doesn't halt. If (contrary contrary) halts, consider what happens when it is evaluated. (halts? contrary contrary) must return #t (because of the specification of halts?), so the true branch of the if is evaluated, that is, (infinite-loop), which never returns. Thus, if (contrary contrary) halts, it doesn't halt, so this case is impossible. So, suppose (contrary contrary) doesn't halt. Then, because of the specification of halts?, (halts? contrary contrary) returns #f, which means that the false branch of the if is evaluated, which just returns the symbol hi. Thus if (contrary contrary) doesn't halt, it halts. Hence this case is impossible too. We conclude that both possible cases lead to a contradiction, and the assumption that halts? exists is false. Thus, no procedure meeting the specification of halts? exists, and the Halting Problem is unsolvable. Can we conclude that the Halting Problem is unsolvable no matter what programming language we consider? If the programming language is equivalent in computational power to Scheme (in an ideal machine with unlimited memory), then the Halting Problem is unsolvable in that language as well. The Church-Turing thesis asserts that the set of computable functions is the same as long as we consider a sufficiently powerful (Turing-complete) programming language, and the Halting Problem is unsolvable in such systems. Notes giving the proof of the unsolvability of the Halting Problem for Turing machines: Halting Problem. It is instructive to compare the procedure (contrary exp) defined above to another procedure, as follows.
(define contrary1
(lambda (exp)
(if (equal? 2 (exp exp))
3
2)))
What happens when we evaluate (contrary1 contrary1)?
The condition for the if involves a recursive call to (contrary1 contrary1),
which calls itself recursively, which calls itself recursively, and
so on.
Thus, (contrary1 contrary1) does not halt.
(Note that if it did halt with a value equal to 2, then it would
have to halt with a value equal to 3, and if it halted with a value
not equal to 2, it would halt with a value equal to 2.
These two cases lead to contradictions, so we conclude that
it cannot halt with any value.)
The difference is that contrary1 evaluates the procedure call
(exp exp), whereas contrary calls the (nonexistent) procedure halts?
on exp and exp, which is supposed to be able to return #t or #f without
evaluating (exp exp).
|
| 9/22/08 | Lecture 9. Turing machines and computability, continued.
Perlis epigram: 83. What is the difference between a Turing machine and the modern computer? It's the same as that between Hillary's ascent of Everest and the establishment of a Hilton hotel on its peak. We constructed a Turing machine to make a copy of its input. It was similar to (and a little more economical than) the machine described in the second half of the notes: Turing machines. We noted that to make a copy of an input with n symbols takes "about" 2n^2 steps of the machine. The Halting Problem: Given a Turing machine M and an input string x, does the machine M halt when started on a tape containing x, in state q1, with the head on the leftmost symbol of x? The question we are interested in regarding the Halting Problem is whether there is a program H to solve it, that is, a program that takes two inputs: a string representing a Turing machine M and a string representing its input x, such that H halts with output 1 if M halts on x and H halts with output 0 if M does not halt on x. This question will be directly addressed in the next lecture, but to develop intuition about it, we consider the Busy Beaver Problem. One simple version of the Busy Beaver Problem considers a Turing machine with n states and two tape symbols: blank and 1. It asks for the largest number of steps that a Turing machine with n states and 2 symbols may take and still *halt* when started on a completely blank tape. It is clearly easy to construct a Turing machine with one state and one symbol that never halts (its only instruction can be (q1, b, q1, b, R)), so the crux of the problem is to run for a large number of steps and still halt. For the problem with n = 2, that is, just 2 states, we came up with the following Turing machine. (q1, b, q2, 1, R) (q2, b, q2, 1, L) (q2, 1, q1, 1, L)When started on a blank tape, the machine goes through the following configurations.
q1: b
^
q2: 1b
^
q2: 11
^
q1: b11
^
q2: 111
^
q1: 111
^
In this depiction, the state of the machine is indicated
to the left of the colon and the tape contents and head position
to the right of the colon.
This machine manages to run for 5 steps and then halt,
because no instruction is defined for state q1 and symbol 1.
This is not quite the best possible, which is 6 steps.
From Heiner Marxen's website about the
Busy Beaver
Problem,
we have the following bounds on S(n), the largest number
of steps a Turing machine of n states and 2 symbols can
run and still halt.
n = # states S(n) = max # steps 1 1 2 6 3 21 4 107 5 (at least) 47,176,870 |
| 9/19/08 | Lecture 8. Let; Turing machines and computability
Handout: 4 page excerpt from A. M. Turing's 1936 paper "On computable numbers with an application to the Entscheidungsproblem." Extra handouts are available in subsequent lectures and outside my office door (414 AKW). When we "nest" lets, the outer let creates a new local environment in which the inner let is evaluated, creating another new local environment whose search pointer points back to the outer let's local environment. For example,
(let ((a 1) (b 2))
(let ((c (+ a b)))
(* 2 c)))
The outer let creates a new local environment in which a is bound
to 1 and b is bound to 2.
In this environment, the expression (let ((c (+ a b))) (* 2 c))
is evaluated, creating another local environment in which c is
bound to 3, and whose search pointer points to the first local
environment.
In this second local environment, (* 2 c) is evaluated, returning
the value 6, which is the value of the inner let, and also the
value of the outer let.
We also considered the more complicated example
(let ((a 1) (b 2))
(let ((a (+ a b)))
(+ a b)))
In this case, the outer let creates a new local environment in
which a is bound to 1 and b is bound to 2.
In this environment, the expression (let ((a (+ a b))) (+ a b))
is evaluated.
This creates a second local environment in which a is bound to 3.
In this second local environment the expression (+ a b) is evaluated.
The lookup of + searches the inner local environment, then
the outer local environment, and then finds + in the top-level
environment.
The lookup of a searches the inner local environment, and finds
the value of a to be 3; the value of a in the outer local environment
is "shadowed".
The lookup of b searches the inner local environment and then
the outer local environment, where it finds the value of b to be 2.
Thus, (+ a b) returns 5, which is the value of the inner let
and also the value of the outer let.
We start the topics of Turing machines, computability and unsolvability. For notes on Turing machines, see Turing machines. |
| 9/17/08 | Lecture 7. Scheme: deep recursion, map and let.
Previously we have seen how to use "flat" recursion on a list to process the top-level elements of the list one by one. If we need to "dive into" lists that are elements of lists, that are elements of lists, and so on, we should consider "deep" recursion. This may be helpful for problem #9 of homework #1. As an example, we write a procedure (count-symbols exp) that takes a Scheme expression exp made up of lists, symbols and numbers, and returns the total number of occurrences of symbols in the expression. Thus we should have (count-symbols '()) => 0, (count-symbols 'hi) => 1, (count-symbols 17) => 0, and (count-symbols '(1 hi there 2 (this ((is)) 3 new 4))) => 5. (In lecture we drew the box-and-pointer diagram for the argument in this last application.) The empty list and a single symbol are base cases for this procedure. A non-empty list is the recursive case: we'd like to find the number of symbols in the car of the list (which itself may be a list) and the number of symbols in the cdr of the list, and then return their sum.
(define count-symbols
(lambda (exp)
(cond
((null? exp) 0)
((symbol? exp) 1)
((list? exp) (+ (count-symbols (car exp))
(count-symbols (cdr exp))))
(else 0))))
To get a better idea of how this procedure works, we can draw
the tree of recursive calls for the evaluation of
(count-symbols '((1 hi is) 2 it)), as follows,
abbreviating count-symbols by c-s.
c-s ((1 hi is) 2 it) => 3
/ | \
/ | \
/ | \
/ | \
+ c-s (1 hi is) => 2 c-s (2 it) => 1
/ | \ / | \
+ c-s 1 c-s (hi is) => 2 + c-s 2 c-s (it) => 1
=> 0 / | \ => 0 / | \
+ c-s hi c-s (is) => 1 + c-s it c-s ()
=> 1 / | \ => 1 => 0
+ c-s is c-s ()
=> 1 => 0
Note that when exp is a number, the "else" condition applies (because it is not
the null list, a symbol, or a list), giving another base case whose value is 0.
In addition to the tree of recursive calls, it is useful to have another view of recursion, the "executive assistant" view, which can be applied to the recursive calls of this procedure as follows. To compute the total number of occurrences of symbols in exp when exp is a non-empty list, we send one executive assistant off to figure out the number of occurrences of symbols in (car exp) and another to figure out the number of occurrences of symbols in (cdr exp). Once we have those two numbers, all we have to do is add them and return the sum. We don't have to trouble ourselves about the details of how those executive assistants get their numbers -- we just assume they do so correctly -- and then we have the simple job of adding the two numbers and returning the result. The piece of code that does this is (+ (count-symbols (car exp)) (count-symbols (cdr exp)))This is evaluated just like any other application -- + evaluates to the built-in add procedure, and its arguments (count-symbols (car exp)) and (count-symbols (cdr exp)) are evaluated, and the resulting numbers are sent to the add procedure, and the sum is returned as the value of the whole cond expression, and then as the value of the whole count-symbols procedure. This way of thinking about recursion is very closely tied to the way we would construct an inductive proof of the correctness of this procedure: we would show that the values for the base cases were correct, and then, assuming that the values for all expressions of "complexity" at most n are correct, we show that the values for all expressions of complexity at most (n+1) are correct. (We may take "complexity" to be the number of numbers, symbols and parentheses in the printed version of the expression.) The procedure map is a built-in procedure to apply a procedure to all the top-level elements of a list and return the list of results. (It does more than this -- have a look at the description in the Scheme reference manual at the MIT Scheme website.) For example, if we evaluate (define square (lambda (n) (* n n)))then (map square '(3 5 6)) => (9 25 36). That is, map takes the procedure square and applies it to each element of the list (3 5 6) and returns the list of results, (9 25 36). We do not have to give the procedure a name to do this. That is, we also have (map (lambda (n) (* n n)) '(3 5 6)) => (9 25 36). We can write our own version of map as follows.
(define our-map
(lambda (proc lst)
(if (null? lst)
'()
(cons (proc (car lst)) (our-map proc (cdr lst))))))
Here proc is an argument that should be a procedure.
The base case is when the list lst is empty -- applying the
procedure proc to every element of the empty list returns the empty list, ().
For the recursive case (lst is non-empty), we apply proc to the first element
of the list lst, and cons the resulting value onto the list returned by
a recursive call of our-map with the same procedure (proc) and the rest
of the list, that is, (cdr lst).
Note that this is similar to the pattern for the procedure we wrote for
the procedure double-each, except that we have "parameterized" the procedure
to the argument proc, instead of having the procedure (to multiply by 2)
as part of the code.
We could achieve the same result as double-each with
(map (lambda (n) (* 2 n)) '(8 4 7)) => (16 8 14).
The tree of recursive calls when we evaluate (our-map square '(3 5 6))
looks as follows (assuming sqaure defined as above.)
our-map square (3 5 6) => (9 25 36)
/ | \
cons 9 our-map square (5 6) => (25 36)
/ | \
cons 25 our-map square square (6) => (36)
/ | \
cons 36 our-map square () => ()
The special form let (which appears in homework #1) is used to create a new local environment to evaluate an expression in. The new local environment is then discarded. For example, (let ((a 1) (b 2)) (+ a b)) => 3. If this expression is evaluated in the top-level environment, it creates a new *local* environment with the symbol a bound to the value 1 and the symbol b bound to the value 2. The "search pointer" for the new local environment points to the top-level environment. Then the "body" of the let, in this case, the expression (+ a b), is evaluated in this new local environment. This is an application, so Scheme tries to look up the symbol +. It is not found in the local environment (because the local environment contains only the symbols a and b), so the search pointer is followed to the top-level environment, where + is found, and its value is the built-in sum procedure. The two arguments a and b are also symbols, but they are found in the local environment, a with value 1 and b with value 2. Then the sum procedure is called with arguments 1 and 2 and returns 3, which is returned as the value of the body of the let, and also the value of the whole let expression. More examples of let will be presented in the next lecture. |
| 9/15/08 | Lecture 6. Scheme: type predicates, equal? and flat recursion
on lists.
The predicate (null? exp) returns #t if exp is the empty list, (), and #f otherwise. Other predicates to test the run-time types of Scheme values are number?, symbol?, boolean?, pair?, list? For now, we will use two predicates to test equality, (= m n) to test whether the number m is equal to the number n, and (equal? exp1 exp2) to test whether more general expressions are equal (including lists). The textbook emphasizes the use of eq? and eqv? -- use equal? for now, and we will get back to eq? and eqv? later in the course. "Flat" recursion on lists is a process that examines or modifies each top-level element of a list in turn, without "diving" into any sublists those elements might contain. An example is the built-in procedure (length lst), which returns the number of top-level elements of the list lst. We can write (our-length lst), our own version of length. An example: (our-length '(a o u)) => 3.
(define our-length
(lambda (lst)
(if (null? lst)
0
(+ 1 (our-length (cdr lst))))))
The base case is the empty list, tested by (null? lst).
The length (number of elements) of the empty list is just 0.
The recursive case is a non-empty list, which has at least
one element.
We call the procedure recursively on (cdr lst), the list lst
with its first element removed, and add 1 to the result.
A call tree representation of the processing for (our-length '(a o u))
is as follows.
our-length (a o u) => 3
/ | \
+ 1 our-length (o u) => 2
/ | \
+ 1 our-length (u) => 1
/ | \
+ 1 our-length () => 0
If you trace our-length and evaluate (our-length '(a o u)), you
will see the procedure calls of our-length with (a o u), then (o u),
then (u), then (), and the procedure returns of our-length of
0, then 1, then 2, then 3.
We can also write a procedure to transform every element of a list. For example, we can write (double-each lst), which takes a list of numbers and doubles each number. As an example (double-each '(8 4 7)) => (16 8 14). This procedure also has the empty list as its base case. Doubling all the elements of the empty list produces the empty list, that is, (double-each '()) => (). The recursive case makes a recursive call to double each element of (cdr lst) and then cons'es in twice the first element of lst to the result.
(define double-each
(lambda (lst)
(if (null? lst)
'()
(cons (* 2 (car lst)) (double-each (cdr lst))))))
The evaluation of (double-each '(8 4 7)) would have the following call tree.
double-each (8 4 7) => (16 8 14)
/ | \
cons 16 double-each (4 7) => (8 14)
/ | \
cons 8 double-each (7) => (14)
/ | \
cons 14 double-each () => ()
This pattern, transforming each element of a list and making
a list of the results, is very common.
We'll see an abstraction for it next time.
Another pattern is searching down the elements of a list until one is found that satisfies some criterion. As an example of that, we'll write the predicate (member? item lst), which returns #t if item is equal (in the sense of the predicate equal?) to a top-level element of lst, and #f otherwise. Scheme has a convention (for the consumption of humans) that names of predicates end in ?, like equal? and member? and null? (A predicate is a procedure that returns #t or #f.) (There is a built-in procedure member, that is similar to our member?, but returns the rest of the list from the match instead of #t -- read more about it in the Scheme specification on the MIT Scheme webpage.) One base case for member? is when lst is the empty list -- the empty list has no elements, so the returned value should be #f. Another base case for member? is when (car lst) is equal to item -- then we can just return the value #t without making any recursive calls. The recursive case is when neither of these is true, that is, when the list is non-empty but its first element is not equal to item -- in this case, we want to continue searching for item in the rest of the list, that is, in (cdr lst).
(define member?
(lambda (item lst)
(if (null? lst)
#f
(if (equal? item (car lst))
#t
(member? item (cdr lst))))))
This has nested if's, and may be easier to read with a cond.
(define member?
(lambda (item lst)
(cond
((null? lst) #f)
((equal? item (car lst)) #t)
(else (member? item (cdr lst))))))
Another useful pattern is to create a list from a number. Consider the following procedure (count-down 4) => (4 3 2 1 blast-off). That is, with input a non-negative integer, the procedure count-down should create a "counting down" list from that integer to the symbol blast-off. Now the base case is when the argument is 0: the result should just be the list (blast-off). In the recursive case, the argument is cons'ed onto the list obtained by a recursive call with the argument minus one.
(define count-down
(lambda (n)
(if (= n 0)
'(blast-off)
(cons n (count-down (- n 1))))))
A call tree for the evaluation of (count-down 3) is as follows.
count-down 3 => (3 2 1 blast-off)
/ | \
cons 3 count-down 2 => (2 1 blast-off)
/ | \
cons 2 count-down 1 => (1 blast-off)
/ | \
cons 1 count-down 0 => (blast-off)
Suppose we had instead written the following.
(define another-count-down
(lambda (n)
(if (= n 0)
'blast-off
(list n (another-count-down (- n 1))))))
Note that we return just the symbol blast-off for the base
case, and use list instead of cons in the recursive case.
To see what happens in this case, we construct a call
tree for the evaluation of (another-count-down 3).
another-count-down 3 => (3 (2 (1 blast-off)))
/ | \
list 3 another-count-down 2 => (2 (1 blast-off))
/ | \
list 2 another-count-down 1 => (1 blast-off)
/ | \
list 1 another-count-down 0 => blast-off
Note that this result is not what we originally
wanted, because of the nested lists.
Some of the problems you will have with your programs will
be of this kind -- nested lists instead of flat lists (or vice versa)
or dotted pairs you didn't want.
In this case, checking your procedure's uses of list constructors
will be helpful.
There is another built-in procedure, (append lst1 lst2), that returns a list containing the top-level elements of lst1 followed by the top-level elements of lst2. For example, (append '(a o u) '(v w)) => (a o u v w). We can write our own version of append as follows.
(define our-append
(lambda (lst1 lst2)
(if (null? lst1)
lst2
(cons (car lst1) (our-append (cdr lst1) lst2)))))
As an example, we consider the call tree for evaluating
(our-append '(a o u) '(v w)).
our-append (a o u) (v w) => (a o u v w)
/ | \
cons a our-append (o u) (v w) => (o u v w)
/ | \
cons o our-append (u) (v w) => (u v w)
/ | \
cons u our-append () (v w) => (v w)
The base case for this procedure is when lst1 is empty.
In this case, the result of appending lst1 and lst2 is just lst2 itself.
In the recursive case, we recursively find the result of appending
(cdr lst1) and lst2, and then cons the first element of lst1, (car lst1),
into the result.
Note the differences:
(cons '(a o u) '(v w)) => ((a o u) v w) (list '(a o u) '(v w)) => ((a o u) (v w)) (append '(a o u) '(v w)) => (a o u v w) |
| 9/12/08 | Lecture 5. Scheme: cond, and, if, and lists.
Perlis epigram #15. Everything should be built top-down, except the first time. Dealing with boolean values. The built-in procedure not exchanges true and false, that is, (not #f) => #t and (not #f) => #t. There are three useful special forms for dealing with boolean values: cond, and, or. Recall that a special form does not follow the usual rules of evaluation for an application. The special form cond gives you a way to avoid nested if's. For example, the procedure sign from last time could be re-written with cond as follows.
(define sign
(lambda (n)
(cond
((< n 0) 'negative)
((> n 0) 'positive)
(else 'zero))))
The arguments of cond form a sequence of conditions and expressions
to evaluate, as follows.
(cond
(c1 e1)
(c2 e2)
.
.
(ck ek)
(else e))
The conditions c1, c2, ... are evaluated in sequence, until
the first one that is true (i.e., not #f) -- then the corresponding
ej is evaluated, and its value is returned as the value of the
cond expression.
In this context, else is a condition that evaluates to true.
Note that no other expressions (the rest of the conditions,
and any of the other expressions ei) are evaluated.
The special forms and, or can be used to combine boolean values. When (and e1 e2 ... ek) is evaluated, the expressions e1, e2, ... are evaluated in sequence, until the first one that is false (i.e., #f). At that point, #f is returned as the value of the and expression, with no subsequent ei's evaluated. If all of e1, e2, ..., ek evaluate to true (i.e., not #f) values, then the value of ek is returned as the value of the whole and expression. For example, (and (= 1 2) (> 4 3)) => #f, evaluating just (= 1 2) to find that this is #f. However, (and (> 4 3) (= 1 2)) => #f, evaluating (> 4 3) and then (= 1 2) to find that the latter is #f. As another example, (and (> 4 3) (< 1 2)) => #t, evaluating both (> 4 3) and (< 1 2), and returning the value of the latter. Because any value other than #f is treated as not #f, there are some legal expressions that are not recommended, for example, (and 3 4) => 4. The special form or is analogous to and, except that it is looking for the first true (i.e., not #f) value, and returns that value as the value of the or expression. If all of the values are #f, then it returns #f as the value of the or expression. Thus (or (= 1 2) (> 4 3)) evaluates (= 1 2) and then (> 4 3), and because the latter is #t, returns #t. Evaluating (or (> 4 3) (= 1 2)) evaluates only (> 4 3), and, finding it #t, returns #t. Evaluating (or (= 4 3) (= 1 2)) returns the value #f. Again, there are legal but not recommended usages like (or 3 4) => 3. l Lists. A list is a finite sequence of Scheme values. The empty list, denoted (), has no elements and length 0. The list containing just the element 19 prints out as (19) and may be quoted as '(19). The list containing the three symbols a, o and u prints out as (a o u) and may be quoted as '(a o u). A list containing two elements, the first of which is the symbol a and the second of which is the list (1 2) is printed out as (a (1 2)) and may be quoted as '(a (1 2)). Box and pointer representations of lists: please see the textbook. List selectors. The two basic list selectors are the built-in procedures car (which returns the first element of a list) and cdr (which returns the list minus its first element.) Thus (car '(a o u)) => a and (cdr '(a o u)) => (o u). These can be iterated, for example, (cdr (cdr '(a o u)) => (u). There are abbreviations for iterations of the selectors, for example (cddr x) for (cdr (cdr x)) and (cadr x) for (car (cdr x)), up to about depth 4. For a one element list, (cdr '(u)) => (). The basic list constructor is cons. This is a built-in procedure of two arguments that allocates a fresh cons cell (a two-part box in the box-and-pointer interpretation) and puts the value of the the first argument in the left part and the value of the second argument in the right part of the new cons cell, and returns a pointer to the cons cell. In terms of lists, if the first argument is an item and the second argument is a list, the resulting value is a list with the item included as the first element of the list. For example, (cons 'a '(o u)) => (a o u), and (cons 1 '()) => (1). However, cons can be used even when its second argument is not a list, for example (cons 1 2). The resulting value is printed out as (1 . 2), a "dotted pair" that is an "improper list", and consists of one cons cell with 1 in its left part and 2 in its right part. For now you can mostly ignore this extra capability of cons, and concentrate on using it when the second argument is a list and you want to include a new element at the front of the list. In this case, the appearance of dotted pairs like (1 . 2) in your output will signify that your list handling code has some flaw. Another list constructor is the procedure list, which takes any number of arguments and evaluates them in sequence and returns a list of their values. For example, (list (+ 1 2) (* 2 3)) evaluates (+ 1 2) to get 3 and evaluates (* 2 3) to get 6, and returns the list of two elements: (3 6). By contrast, using quote, say '((+ 1 2) (* 2 3)), returns ((+ 1 2) (* 2 3)), which is a list of two elements, the first of which is a list of three elements (the symbol +, the number 1 and the number 2) and the second of which is a list of three elements (the symbol *, the number 2 and the number 3). |
| 9/10/08 | Lecture 4. Scheme: number predicates, recursion, quote.
Perlis epigram #12. Recursion is the root of computation since it trades description for time. To make if useful, we need some predicates. Here are some predicates to compare numbers: =, >, <, >=, <=, signifying equal, greater than, less than, greater than or equal to, and less than or equal to. These are symbols defined to be built-in procedures, and have ordinary application syntax. Thus (= 3 3) => #t, (> 4 3) => #t, and (<= 100 99) => #f. (Recall that MIT Scheme will print out #f as ().) We use these to write a procedure for the absolute value function as follows. Recall that the absolute value of a non-negative number is the number itself, while the absolute value of a negative number is the negative of that number. That is, |3| = 3, |0| = 0 and |-3| = 3. We may write a procedure named abs to do this as follows.
(define abs
(lambda (x)
(if (< x 0)
(* -1 x)
x)))
If the argument x is less than 0, then we multiply it by -1
and return the negative of x, otherwise, we return x unaltered.
Note that -x does not work to give us the negative of x,
but (- 0 x) or just (- x) would work.
A classical recursive function: factorial. The mathematical definition of the factorial of a number, n!, is as follows: if n = 1 then n! is 1, otherwise it is n * (n-1)!. We can directly mimic this definition as a recursive (that is, self-calling) Scheme procedure as follows.
(define factorial
(lambda (n)
(if (> n 1)
(* n (factorial (- n 1)))
n)))
Then we have (factorial 1) => 1, (factorial 2) => 2, (factorial 3) => 6,
(factorial 4) => 24, and so on.
One way to see what is happening with a procedure is to trace it. That is, if we evaluate (trace factorial) and then (factorial 4), the trace will print out every call to and return from the factorial procedure, roughly as follows.
(factorial 4) is called
(factorial 3) is called
(factorial 2) is called
(factorial 1) is called
(factorial 1) returns 1
(factorial 2) returns 2
(factorial 3) returns 6
(factorial 4) returns 24
To get rid of a trace, we use (untrace factorial).
Another way to think about this is to draw a tree of the recursive
calls of factorial.
Two more arithmetic functions: (quotient m n) returns the integer quotient of m divided by n, and (remainder m n) returns the integer remainder of m divided by n. Thus, (quotient 7 3) => 2, and (remainder 7 3) => 1 because when we divide 7 by 3 we get a quotient of 2 and a remainder of 1. Now we can write two procedures, one to return the last decimal digit of a number, and one to return the first decimal digit of a number. Observe that the last decimal digit of a number like 347 is just the remainder when we divide the number by 10: the quotient is 34 and the remainder is 7. This is also true of numbers less than 10, for example, 7 divided by 10 has a remainder of 7. We may therefore write a procedure for the last digit as follows.
(define last-digit
(lambda (n)
(remainder n 10)))
The procedure for the first digit involves recursion. Starting with 347, if we divide by 10 we get a quotient of 34, and if we divide this by 10, we get a quotient of 3, which is the correct first digit of 347. The base case is a number less than 10 -- it is its own first digit. Thus, the following procedure works.
(define first-digit
(lambda (n)
(if (< n 10)
n
(first-digit (quotient n 10)))))
Another special form is quote, which inhibits the normal evaluation rules. Thus (quote fluffy) returns the (unevaluated) symbol fluffy. An alternative syntax for this is 'fluffy, that is, a single quote followed by a Scheme expression. As an example of the use of this, we write a procedure (sign n) that returns the symbol positive if n is greater than 0, the symbol zero if n is equal to 0, and negative if n is less than 0, as follows.
(define sign
(lambda (n)
(if (< n 0)
'negative
(if (> n 0)
'positive
'zero))))
Nested if's like this can get pretty confusing; the special form cond
can help you keep such code more readable.
|
| 9/8/08 | Lecture 3. Scheme: lambda, booleans, if.
Perlis epigram #3. Syntactic sugar causes cancer of the semicolon. Perlis epigram #55. A LISP programmer knows the value of everything, but the cost of nothing. The special form lambda lets us create procedure values. For example, when we evaluate the expression (lambda (n) (* n n)), the resulting value is a procedure that takes one argument and returns the square of its argument. Here lambda is a keyword signifying that this is a lambda expression, (n) is the list of formal arguments, and the expression (* n n) is the body of the procedure, which indicates what to do with the actual arguments. We can use such an expression in an application, for example, ((lambda (n) (* n n)) 3) => 9. We can also use the special form define to give our procedure a name, for example:
(define square
(lambda (n)
(* n n)))
Evaluating this define expression puts the symbol square in
the top-level environment with a value consisting of a procedure
that takes one argument and squares it.
This is a little different from other programming languages,
in which the creation and naming of a procedure is one monolithic
piece of syntax.
We can now use recursion to write a procedure that never halts.
(define infinite-loop
(lambda (n)
(infinite-loop n)))
In fact, we can simplify our infinite-loop program to take no argument as follows.
(define simpler-infinite-loop
(lambda ()
(simpler-infinite-loop)))
Note that in this case the list of formal arguments is empty, (),
and the application to call the procedure gives only the name
of the procedure and no arguments.
We would invoke this procedure by evaluating (simpler-infinite-loop).
How can we write procedures that use recursion, but actually halt? We will need to distinguish the base case(s) from the recursive case(s) by testing the argument(s) and doing different things based on the results of the test. We need boolean values, which in Scheme are #t and #f, and a way to do conditional evaluation. One way to do conditional evaluation is with the special form if. For example, (if #t 3 4) => 3, and (if #f 3 4) => 4. Why is if a special form rather than an ordinary application? The answer is that if does not evaluate all of its arguments. It either evaluates the condition and the true branch (if the condition comes out to be not #f), and DOES NOT evaluate the false branch, or, it evaluates the condition and the false branch (if the condition comes out to be #f), and DOES NOT evaluate the true branch. So, for example, (if #t 3 (simpler-infinite-loop)) => 3, whereas (if #f 3 (simpler-infinite-loop)) goes into an infinite loop, and similarly (if #t (simpler-infinite-loop) 4) goes into an infinite loop, whereas (if #f (simpler-infinite-loop) 4) => 4. Note that MIT Scheme treats #f as equivalent to the empty list, (). |
| 9/5/08 | Lecture 2. Scheme: constants, applications, symbols, define.
Perlis epigram #17. If a listener nods his head when you're explaining your program, wake him up. Perlis epigram #23. To understand a program, you must become both the machine and the program. The goal of the first few lectures is to install the rules of a Scheme interpreter in your head, consistent with Perlis's epigram #23. The Scheme interpreter is a program that implements a Read, Evaluate, Print Loop (REPL). It reads in a Scheme expression, evaluates it according to the rules we are about to learn, and prints out the resulting value, and then reads in another expression, etc. First rule: a constant evaluates to itself. For example, 19 => 19. The arrow (=>) is read as "evaluates to". Second rule: evaluating applications. An application like (+ 19 13) is evaluated by evaluating each member of the list. The symbol + evaluates to the built-in procedure to add numbers, the constants 19 and 13 evaluate to themselves, and then the procedure is called with the other values as its arguments. The value returned by the procedure, in this case 32, is returned as the value of the application. That is, (+ 19 13) => 32. Third rule: evaluating symbols. To evaluate a symbol (like +), look it up in the relevant environment. The meaning of "relevant" will be clarified, but right now we think only of the top-level environment. When you first enter the Scheme interpreter, there is a global or top-level environment. This is a table that contains the pre-defined symbols (eg, +) and their values (eg, the built-in procedure to add numbers.) Fourth rule: the special form "define". A special form is invoked by a keyword (like "define") and does not follow the standard evaluation rules for applications. If after you enter Scheme you evaluate the expression (define fido 19), this *does not attempt* to evaluate the symbol fido but does evaluate its second argument in the usual way. The symbol fido is then placed in the table for the top-level environment with the value 19. At this point, the symbol fido can be used in expressions, so that (- fido 10) => 9. If we then evaluate (define fluffy (+ fido 12)), the symbol fluffy is placed in the table for the top-level environment with the value 31. If we then re-define the value of fido by (define fido 20), the value of fluffy is still 31. Error messages. If you type an expression like (13 + 19), the error message will be "The object 13 is not applicable." In other words, the interpreter takes this to be an application, but the first element of the list, 13, does not evaluate to a procedure (i.e., is not "applicable.") Parentheses are not innocuous in Scheme. If you type the expression +, its value is #[arity-dispatched-procedure 1] or somesuch. This is not an error message, but an external representation of the built-in procedure for addition. In Scheme there is no reason that a procedure can't have another procedure as an argument; we'll make use of this capability soon. |
| 9/3/08 | Lecture 1. Introductory Lecture.
Perlis epigram #19. A language that doesn't affect the way you think about programming, is not worth knowing. Discussion of the syllabus for 201 (paper or website). Collection of signups for ID validation for after-hours access to AKW and the Zoo (paper, but you can also sign up by going to 009 AKW and signing up there.) Description of the overall structure of the course and why it is taught in Scheme. Assignment #0 (paper): read Introduction and 2 of the 8 articles from the September 2008 Scientific American, write a 1 page response -- turn in on paper Monday (9/8) and email to DA. Extras of paper handouts (eg, Assignment #0) will be available outside DA's office (414 AKW) as well as in subsequent classes. The importance of notation. In Ancient Egyptian fraction notation there was a special symbol for 2/3, but all other fractions were expressed as sums of unit fractions (1/n for an integer n > 1) with different denominators. For example, 2/5 might be expressed as 1/3 + 1/15. Idea: choose the smallest positive integer n such that 2/5 is greater than or equal to 1/n, subtract 1/n from 2/5 and repeat if necessary. That is, choose the smallest integer n greater than 5/2, namely n = 3, subtract 1/3 from 2/5 and get the result 1/15 and terminate. One issue about this algorithm: will it always terminate? That is, if we start with any reduced proper fraction a/b, where a and b are positive integer, will this process express a/b as a finite sum of unit fractions with different denominators? (Reference: the Rhind Papyrus, recording problems and their solutions from about 4000 years ago.) |