OK, so you had an extra lecture, and I was given a change to fill it with something I liked. I had several things that I wanted to talk about - all are hard enough to make you think a little. Here is a brief review of what I didn't do: ================================================== * Meta Object Protocols. - Main idea: give the programmer of a language X the power to modify the way X behaves. - The usual way of doing this is to have certain `hooks' visible for programmes (ex: a hook that Scheme will run before evaluating code). - What you want for this is some way that the language usually behave and allowing the user to customize certain specific points. - A natural way of taking the way something behaves by using most of its default behavior but modifying small aspects is OOP. - So the idea of MOP is exposing some behavior to the programmer, allowing him to specialize this behavior further. - This is done by having the usual things you manage in an OOP environment be simple instances of "meta classes" - these classes determine how these "meta objects" behave the same classes determine how instances behave. - Some things you can do with it (slots on demand, persistent objects, C++ style inheritance, etc). ================================================== * Lazy evaluation (normal order). - What is the difference exactly between this and what we have in Scheme? (Eager evaluation, or applicative order). - Some expression may get stuck in an infinite loop when you use eager evaluation, and not in lazy evaluation. - This is because you must evaluate every argument in a an eager language. But if Scheme were lazy, then (define (if x y z) (cond (x y) (else z))) was perfectly fine. - Some computations become more efficient since you never evaluate something you don't need, but other computations become less efficient. - Generally, modifies the way you program (for example - tail recursion might not be better). - Some problems get very simple solutions using lazy languages, for example - the N-queen problem in Haskell. - Streams are one compromize - provide a lazy data constructor in an eager language. We could have more structures like have cons be lazy in both its arguments or a version of make that will be lazy. - You could even use the MOP to design such lazy classes (with the help of some macro voodoo). ================================================== So the final subject choise was continuations. -------------------------------------------------- * What are continuations? At any given point of computation, you can say that we are operating inside some context. In Scheme all expressions have value, and the cotext is usually what waits for a value to be produced. For example, when you enter this expression: (* (f (+ 1 2)) 3) then at the point of evaluating (+ 1 2), the context that waits fo an answer the f's application, the context of (f (+ 1 2)) is the multiplications, and the context of the multiplication is the toplevel loop. This is a little hard to understand, but a little practice usually clarifies it. This is a Scheme standard built-in with the lovely name: call-with-current-continuation, but usually call/cc as well. (call/cc f) captures the current continuation as a procedural object and transfers it to f. This means that we have ways to manage continuations as normal Scheme objects. The function that receives this continuation can do whatever it wants with it - it can 1. apply it which is the same as returning a value from the call/cc context, 2. it can store it somewhere, 3. and it can return some other value which will be the result of the call/cc expression. Note that 3 is very differnet than 2. Some examples to make things clear: ==> (+ 1 (call/cc (lambda (k) 2)) 3) 6 ==> (+ 1 (call/cc (lambda (k) (k 2))) 3) 6 ==> (+ 1 (call/cc (lambda (k) (+ (k 2) 66))) 3) 6 ==> (+ 1 (call/cc (lambda (k) (/ (k 2) 0))) 3) 6 ==> (define (p x) (echo '< x '>) x) ==> (+ 1 (call/cc (lambda (k) (/ (k 2) 0))) (p 3)) < 3 > 6 ==> (define k1 #f) ==> (+ 1 (call/cc (lambda (k) (set! k1 k) 2)) (p 3)) < 3 > 6 ==> (k1 22) < 3 > 26 ==> (define x (call/cc (lambda (k) (set! k1 k) 1))) ==> x 1 ==> (k1 2) ==> x 2 As demonstrated, we can do anything with continuations, and we can capture toplevel continuations as well. MzScheme has a shortcut - instead of (call/cc (lambda (k) ...)) you can do (let/cc k ...) -------------------------------------------------- * What do we get by using continuations? Continuations were introduced into Scheme since they go well with the spirit of the language - it is a small feature that allows many things. Basically, we can do all kinds of control structures. For example - non-local exit: what if we want an abort function that will immediately return to the repl? We can define abort as the top level continuation, but this is a little tricky... ==> (define abort (let/cc k k)) ==> abort # ==> (+ 1 2 (abort 3)) ==> abort 3 One way that we can solve this is by renaming that as abort1, and then using this definition of abort: (define (abort) (abort1 abort1)) But this is tricky, plus it doesn't allow us to return some value. The better way of solving this is: ==> (define abort #f) ==> (let/cc k (set! abort k)) ==> (+ 1 2 (abort 3)) 3 ==> (abort (+ 1 2 (abort 6))) 6 Another example: lets say we want to get the product of a list... (define (prod l) (if (null? l) 1 (* (head l) (prod (tail l))))) This is inefficient when the list can contain zeros. So, we might do this: (define (prod l) (cond ((null? l) 1) ((zero? (head l)) 0) (else (* (head l) (prod (tail l)))))) which is better since it stops as soon as it reaches a zero, but still - it returns this zero which will get multiplied redundantly by previous values... So, we might do this: (define (prod l) (define (loop l a) (cond ((null? l) a) ((zero? (head l)) 0) (else (loop (tail l) (* a (head l)))))) (loop l 1)) but this is more complicated than the original expression. (Well, not to much in this case, but other cases can be much worse.) This has a natural solution with continuations: ==> (define (prod l) (cond ((null? l) 1) ((zero? (head l)) (abort 0)) (else (* (head l) (prod (tail l)))))) ==> (prod '(1 2 3 4 0 5 6 7 8 9)) 0 or with a local escape: (define (prod l) (let/cc escape (define (prod l) (cond ((null? l) 1) ((zero? (head l)) (escape 0)) (else (* (head l) (prod (tail l)))))) (prod l))) Other variations on this are natural - for example, search a binary tree and return one element, do something with it and then retrieve the next one. Using continuations is also equivalent to the power we get by using goto statements, for example, we can directly translate this fictional code: (define (prod list) (let ((p 1)) loop: (when (null? l) (goto out:)) (set! p (* p (head list))) (set! list (tail list)) (when (zero? p) (goto out:)) (goto loop:) out: (echo "Product =" p))) to something concrete: ==> (define (prod lst) (let ((p 1) (loop: #f)) (let/cc out: (let/cc loop (set! loop: loop)) (when (null? lst) (out:)) (set! p (* p (head lst))) (set! lst (tail lst)) (when (zero? p) (out:)) (loop:)) (echo "Product =" p))) ==> (prod '(1 2 3 4)) 24 ==> (define a (list 0 1 2 3)) ==> (set! a (append '(1 2 3 4 5 6) (append! a a))) ==> (prod a) 0 [Note that this translation uses continuations with no values, this is because we don't use their values.] -------------------------------------------------- * Cute things that can be implemented using continuations. So one thing that we can do with abort is make it a function that can also continue the aborted computation: just before calling the toplevel continuation, we save the local continuation and use it to continue computation: ==> (define toplevel-k #f) ==> (let/cc k (set! toplevel-k k)) ==> (define continue #f) ==> (define (abort msg) (let/cc cont (set! continue (lambda () (set! continue #f) (cont #f))) (toplevel-k (list 'abort: msg)))) ==> (define (fact n) (abort (list 'fact n)) (if (<= n 0) 1 (* n (fact (sub1 n))))) ==> (fact 4) (abort: (fact 4)) ==> (continue) (abort: (fact 3)) ==> (continue) (abort: (fact 2)) ==> (continue) (abort: (fact 1)) ==> (continue) (abort: (fact 0)) ==> (continue) 24 ==> (continue) ERROR - continue is now #f One thing that we can do is change the break handler (there is one in MzScheme) so it will call this abort function - the result is that we will be able to stop continuation, examine some global variables and then resume. This can be extended very easily to create something realy nice, just give me continuations and a queue! (anyone ever seen McGyver?) ==> (define processes '()) ==> (define (enqueue-process! p) (set! processes (append processes (list p)))) ==> (define (dequeue-process!) (let ((p (head processes))) (set! processes (tail processes)) p)) ==> (define (more-processes?) (not (null? processes))) ==> (define (done) (if (more-processes?) ((dequeue-process!)) (error "all done"))) ==> (define (yield) (let/cc me (enqueue-process! me) (done))) ==> (define (fork f) (let/cc me (enqueue-process! me) (f) (done))) And now: ==> (define (make-foo name n) (define (loop n) (echo "name:" name n) (yield) (sleep 1) (if (zero? n) (done) (loop (sub1 n)))) (lambda () (loop n))) ==> (define (main) (fork (make-foo "foo" 6)) (fork (make-foo "bar" 4)) (fork (make-foo "baz" 2)) (done)) ==> (main) name: foo 6 name: bar 4 name: foo 5 name: baz 2 name: bar 3 name: foo 4 name: baz 1 name: bar 2 name: foo 3 name: baz 0 name: bar 1 name: foo 2 name: bar 0 name: foo 1 name: foo 0 all done This is a complete cooperative threading system in 23 lines of code! And like McGyver, give me an extra bubble gum and I'll do anything... All you need now is a timer function that will call yield and you get preemptive processes. (Unfortunately, there is no such built-in mechanism, and although it is not hard to implemet one, it will be too much for here.) -------------------------------------------------- * How do we get continuations? So here we develop a pattern matcher for regular expressions. When we do this, we want to use more general continuations - ones of different kinds. Specifically, we want to have a do-match program that will get: 1. a token list to be matched (a list of symbols), 2. a pattern to match against, 3. a success continuation, 4. a failure continuation. We need continuations to do "backtracking" - when we fail to match with some pattern, others might be used on the same sequence. For example, if out expression is (A+AB)C, and we get an input of ABC, then we first try to match A, succeed, but then we try to match C and fail so we must backtrack to the or (+) point and try the other branch. So we define the success continuation as a function that gets (1) the rest of the sequence that needs to be parsed and (2) a failure continuation that specifies what to do in case we fail. We also get a failure continuation which is a function with no arguments that is used to abort matching in case we fail. [I know this doesn't sound too clear, looking at the way this is implemented might help a bit but it is hard to understand any way you look at it.] ;; ----------------------------------------------- ;; define do-match as a generic funcion (defgeneric (do-match xs pattern success fail)) ;; the default case matches some token (defmethod (do-match xs (p ) s f) ;; if we do have a token left (if (and (not (null? xs)) ;; and it is the same one (eq? (head xs) p)) ;; then we succeeded - use the success ;; continuation with what's left of the input ;; and the same failure continuation (s (tail xs) f) ;; otherwise we failed - use the failure ;; continuation to do this (f))) ;; we use objects to represent patterns (defstruct ) ;; define the "or" pattern with two parts (defstruct (<~or> ) p1 p2) (define ~or make-~or) (defmethod (do-match xs (p <~or>) s f) ;; basically, we try to match the given input (do-match xs ;; with the first part of the or (~or-p1 p) ;; and success is simply using the ;; given continuation s ;; but failure is different: (lambda () ;; if we fail in the first part, ;; then the continuation in that ;; case tries to match the input ;; with the second part of the or ;; pattern, if *that* fails or ;; succeeds, then we use our ;; continuations (do-match xs (~or-p2 p) s f)))) ;; define a sequence of two patterns (defstruct (<~seq> ) p1 p2) (define ~seq make-~seq) (defmethod (do-match xs (p <~seq>) s f) ;; to match a sequence we first match the input (do-match xs ;; with the first part of the sequence (~seq-p1 p) ;; if we succeed (lambda (xs f) ;; then we match the second part ;; using the same success ;; continuation we received but ;; using the failure continuation ;; that we were given now (note that ;; xs is whats left now) (do-match xs (~seq-p2 p) s f)) ;; if we fail with matching the first ;; part, we simply abort f)) ;; a structure that represents repeated patterns, ;; zero ot more times (defstruct (<~rep> ) p) (define ~rep make-~rep) (defmethod (do-match xs (p <~rep>) s f) ;; to much the repeated sequence P... we try to ;; match the input with (do-match xs ;; a sequence of P and P... itself, ;; note that the order here is ;; important - we must try P because ;; otherwise we end in an infinite ;; loop (~seq (~rep-p p) p) ;; success is nothing special s ;; but if that fail, we can say that ;; this is actually success if we ;; don't take anything from the input ;; since it means *zero* or more times (lambda () (s xs f)))) ;; this is a utility function that turns a string ;; into a token (symbol) list (define (string->symbols str) (map string->symbol (map string (string->list str)))) ;; this is the entry point (define (match x pattern) ;; it uses do-match on the input sequence ;; followed by a $ token (do-match (append (string->symbols x) '($)) ;; and adds a terminating $ to the ;; patern as well, this will force it ;; to get an exact match (~seq pattern '$) ;; this is the toplevel success ;; continuation (lambda (xs f) #t) ;; and the toplevel failure (lambda () #f))) ;; try some trivial tests (match "a" 'a) (match "a" (~or 'a 'b)) (match "a" (~seq 'a 'b)) (match "ab" (~seq 'a 'b)) (define ab* (~rep (~seq 'a 'b))) (match "a" ab*) (match "ab" ab*) (match "aba" ab*) (match "abab" ab*) (match "" ab*) ;; this is the example we talked about earlier (match "abc" (~seq (~or 'a (~seq 'a 'b)) 'c)) ;; and this is a harder example (define foo (~rep (~or (~seq 'a 'b) (~seq 'a (~seq 'b 'a))))) (match "" foo) (match "ab" foo) (match "aba" foo) (match "ababa" foo) (match "abaaba" foo) (match "abaaaba" foo) ;; ----------------------------------------------- What we did here is called programming with continuation passing style (CPS). We basically created our own continuations manually and used them. Usually the built in call/cc (or let/cc) are enough, but this case required a special, designer continuations. We can actually convert any code so it builds its own continuations, by something that is called CPS transformation. We basically turn every expression in the language into a function that takes a continuation, evaluates something, and sends the result to this continuation. Here is a quick set of rules to do CPS transformation on very simple Scheme expressions (this is the Lambda Calculus subset): x' == (lambda (k) (k x)) (lambda (x) E)' == (lambda (k) (k (lambda (x) E'))) (E1 E2)' == (lambda (k) (E1' (lambda (x1) (E2' (lambda (x2) ((x1 x2) k)))))) There are several important notes here: * CPS transformation return expressions that are always tail recursive. * This was also the case with the matcher above. * If you'll try to follow the evaluation of a CPS transformed code, you'll see that the continuation keeps growing when there are things to remember on the evaluation stack. * We can make slight variations on the above to get different evaluations - right to left, or even lazy evaluation. So two major points that continuations are related to are: 1. The evaluation stack - continuations actually collect stuff to remember the same way that a CPU uses stack. This is also the reason why implementations of continuations are usually done by aking "snapshots" of the run-time stack. 2. Continuations can be used to store and use future computations, so they give us a way to manage non-determinism conveniently, like the above matcher. (There is a very deep logical background to this.)