Revision: Dec. 6, 2001 (1000 AM) Problem 4a (Remove all occurrences of A from the Alst) ---------- <<< Original Scheme Code >>> (define remove* (lambda (A Alst) (cond ( (null? Alst) '() ) ( (pair? (car Alst)) (cons (remove* A (car Alst)) (remove* A (cdr Alst))) ) ( (eq? (car Alst) A) (remove* A (cdr Alst)) ) (else (cons (car Alst) (remove* A (cdr Alst))) ) ))) <<< Example >>> (remove* 3 '(2 (3 4) (5 3))) should be '(2 (4) (5)). (remove* 3 '(2 (3 4) (5 3))) ==> (cons 2 (remove* 3 '((3 4) ( 5 3)))) ==> (cons 2 (remove* 3 '(cons (remove* 3 '(3 4)) (remove* 3 '((5 3)))))) i.e., it recursively handles lists with sublists. <<< Review >>> We seek a CPS (Continuation-Passing Style) version in which we add a parameter that acts as a "function" accumulator and all computations are delayed until we have a lambda expression of the form (K E) where K is arbitrary and E is a lambda expression. There are 2 extreme forms of E that need to be produced until we reach the last step of the recursion in basic CPS: [1] E has no embedded function definitions (i.e., E contains only car, cdr, cons, and atoms) [2] E has embedded function definitions (i.e., E contains lambda expressions of the form "(\ (x) (E))" [ Note: I am using '\' to represent 'lambda'. ] As part of the last recursive step, E will be reduced to form 1 returning the form (K E) where E is the solution we seek to the orginal problem. We can easily recover the "value" of the expression from form [1] by replacing K with the identity function: (id x) = x or id = ( \ x (x) x ) = ( lambda x (x) x ) i.e., (id E), apply id to E. <<< What We Know In General >> THEOREM 1: Let f be our original non-CPS function and c be its CPS form. Then, (K (f x ... )) = (c x ... K) for any K. [ "..." means that there could be more parameters ] Examples: (K (fact n)) = (fact-cps n K) (K (gcd* L)) = (gcd*-cps L K) [ See Continuation lecture notes. ] During the recusion of the gcd*-cps evaluation of the GCD* of '(20 30 40), the following continuation will be created: ((lambda (N) ( (lambda (N) (K (gcd 20 N))) (gcd 30 N)) ) 40) | | |------------| | | |-------------------------| | |-----------------------------------------------------| Note that it contains other continuations. Each continuation is a promise that if you later compute a value N, I can compute the rest of the expression; i.e., the innermost continuation is the first promise, the next is the second and so forth as we work our way out. During continuation construction we build this final continuation. Then, we evaluate it moving inward (left-to-right). If we evaluate this expression, we end up with '(K 10). Now, if K = id, (K 10) ==> 10 and we recover the value we want. Note that in the gcd* example, a function is being built. If you look at this function at an arbitrary time, you see: ((lambda (N) ((lambda (N) ((lambda (N) ... ) When the expression is finally applied to a value at the end of the recursion, the reduction works its way inward (from left-to-right). In effect, we expect to see a "delayed evaluation" in the continuation; i.e., if you bind a value V to the parameter of the outer lambda, the result should be (K V') where V' is the function value IF the remainder of the computation produced the value V. The continuation is a promise that if you apply it to a value V, it will produce the corresponding value of the function. If that V is indeed the correct future value of the rest of the computation, the function application will yield (K V') where V' is the value you seek. Note that the form of the continuation for 'fact-cps' looks like it only contains one lambda no matter how big the original parameter was: (lambda (N) (K (* 4 (* 3 (* 2 N))))) [*] (lambda (N) (K (* 5 (* 4 (* 3 (* 2 N)))))) But that's because there is a partial evaluation that occurs at each step. KEY IDEA: A continuation is a function F that looks like: F = (lambda (V) (E)) and represents a promise: if you apply it to a correct value (to be computed in the future), the result will be (K V) where V is the value of the non-CPS expression. Converting to CPS form involves correctly modifying E based on current knowledge of the arguments so that in the end, application to a value will result in a correct evaluation. <<< Problem 4a >>> How is Problem 4a different from the "fact" and "gcd" examples? 1) It can throw away some atoms that it encounters AND it may continue computing more continuations after throwing away an atom. [ Well, that's expected since removing is part of its specification. But note that the fact example didn't throw away any numbers. While the gcd example only threw away values if it encountered a 1 and immediately stopped evaluating (since then it knew to quit early and return 1). ] 2) It traverses a non-linear structure (i.e., a tree). Difference 1 is a small generalization of the gcd*-cp example. Difference 2 is a more difficult situation to handle. <<< Difference 2 >>> The original algorithm in pseudo-code form (I use r* for remove*, Miranda-like syntax, L instead of Alst, and B for an atom not equal to A) leads to the 4 cases: 1) (r* A '()) = '() # L is empty 2) (r* A (cons A L)) = (r* A L) # Hd of L is A 3) (r* A (L1 L2)) = (cons (r* A L1) (r* A L2)) # Hd of L is sublist 4) (r* A '(B L)) = (cons B (r* A L)) # Otherwise The right-hand side is the E part of (K E) that we seek. In fact, if we just returned (K E) where E is the right-hand side we almost have our solution. The problem is that the RHS in cases 2-4 contains calls to r* and therefore would create additional context which we are trying to avoid (remember the data accumulator in the fact example). we want a solution with A NULL CONTEXT and A FUNCTION ACCUMULATOR. <<< The Conversion Process >>> Let's convert the easiest expressions to CPS first (case 3 is the most difficult): Case 1) (r* A '()) = '() Return (K '()). This happens at the end of a list. K should contain all of the necessary information to complete the entire computation of a list. Also note that there can be many lists because we can have a tree. Case 2) (r* A (cons A L)) = (r* A L) Theorem 1 says that we can return: (c* A L K) and the result will be identical to (K (r* A L)). Here, c* is the CPS form of r* assuming that c* can be written properly. Case 4) (r* A '(B L)) = (cons B (r* A L)) We want a function such that if we knew the value of a future computation (i.e., (r* A L)), we could compute the value of the expression. The following is such a function: (\ (x) (K (cons B x))) [ Again: "\" is "lambda". ] Note that this function is correct since: (K (r* A '(B L)) ) = (c* A '(B L) K) # by definition = ( (\ (x) (K (cons B x))) (r* A L) ) # substitution = (K (cons B (r* A L))) # reduction So, the proper return value for the c* definition is the expression: (c* A L K) where K = (\ (x) (K (cons B x))) or (c* A L (\ (x) (K (cons B x))) ) Note that when the lambda function is used, x will be replaced by the values to the right of B; i.e., values in the remainder of the list that we have not examined yet. Case 3) (r* A (L1 L2)) = (cons (r* A L1) (r* A L2)) This is the most difficult case, because a sublist must be traversed. But it is exactly like Case 4 but with B replaced by (r* A L1) in our definition. The case is difficult because we aren't allowed to just replace the B in Case 4 with (r* A L1): that expression will result in a non-null context. But we know the general form of the expression that needs to be eventually evaluated: (K (cons V1 V2)). V1 is due to the (future) computation on the sublist L1, and V2 is due to the (future) computation on the remainder of the list L2. If we could easily express this idea, then we would have the expression we are looking for. We begin by noting that when we finally do function application (starting from the outermost lambda expression), the values produced in our example will first be '((5)) which will then be used to build an expression like: ... other stuff ... (cons (K r* 3 '(3 4)) '(5)) i.e., it looks like: ... other stuff ... (cons K1 '(5)) where K1 = '(K (4)). Once we get past L1, we want to return: (c* A L2 K2) where K2 embodies the computations associated with processing L1 and all of the preceding elements of the original list. K2 is: K2 = (\ (V2) (K (cons V1 V2))) where V1 will be the value associated with L1; i.e., it is a future computation which will be constructed as we traverse L1. This means that the return value that we seek is: (c* A L1 K1) where K1 = (\ (V1) (c* A L2 (\ (V2) (K (cons V1 V2))))) Note that when we reach the end of the sublist L1, the evaluation will be (K3 '()) where K3 is built from K1; i.e., K3 will look like: K3 = (\ (V1) (c* A L2 (\ (V2) (K ((V1a V1b ...) V2))))) where V1a, V1b, ... are the elements of the sublist L1 that are not equal to A. Summarizing: 1) (c* A '() K) = (K '()) # Empty list 2) (c* A (cons A L) K) = (c* A L K) # Hd of L is A 3) (c* A (L1 L2) K) = (c* A L1 K1) # Hd of L is sublist where K1 = (\ (V1) (c* A L2 (\ (V2) (K (cons V1 V2))))) 4) (c* A '(B L) K) = (c* A L (\ (x) (K (cons B x))) ) # Otherwise <<< The Scheme Definition >>> We translate the above to Scheme by using lambda for '\', using 'cond' instead of case statements, and using remove*-cps instead of c*: (define remove* (lambda (A L) (remove*-cps A L (lambda (V) V)))) (define remove*-cps (lambda (A L K) (cond ( (null? L) (K '()) ) ( (eq? (car L) A) (remove*-cps A (cdr L) K) ) ( (pair? (car L)) (remove*-cps A (car L) (lambda (V1) (remove*-cps A (cdr L) (lambda (V2) (K (cons V1 V2))))))) (else (remove*-cps A (cdr L) (lambda (V) (K (cons (car L) V)))) ) )))