Explain the continuation example on p.137 of The Little Schemer

Viewed 2443

The code in question is this:

(define multirember&co
  (lambda (a lat col)
    (cond
     ((null? lat)
      (col (quote ()) (quote ())))
     ((eq? (car lat) a)
      (multirember&co a
                      (cdr lat)
                      (lambda (newlat seen)
                        (col newlat
                             (cons (car lat) seen)))))
     (else
      (multirember&co a
                      (cdr lat)
                      (lambda (newlat seen)
                        (col (cons (car lat) newlat)
                             seen))))))

I've stared at this all day but I can't quite seem to understand it. When you recur on the function you are re-defining col but in the examples they seem to use the original definition. Why wouldn't it change. How can you recur on it without passing in the parameters newlat and seen.

It's hard to explain my question because I seem to just be missing a piece. If perhaps someone could give a more explicit walk-through than the book I may be able to understand how it works.

7 Answers

I have struggled myself, to understand what is happening inside the multirember&co, for quite a while. The problem is that the moment I thought I've got it - next task/example proved I have not.

What helped me was putting together a visual representation of what is happening (for me text walkthroughs are tough to grasp, for some reason).

So, I have put together two flow-charts:

One, just showing the relations between different steps of recursion:

Visual walkthrough showing relations


And another one, reflecting actual values:

Visual walkthrough with values

Now, whenever I feel like I'm loosing 'the thread of an argument' again, I just refer to this flow-charts and it puts me back on track.

Another thing I've understood after looking at the 'whole picture' via flow-chart, is that a-friend function is simply checks whether seen holds any values or not (though it returns it the other way around i.e. #f when there values in seen and #t when seen is empty, which might be confusing.

P.S.: I did similar flow-charts for evens-only*&co, which appears later in the book.

Let's use some equational pseudocode with some parentheses omitted for clarity (so, we write f x y for the call (f x y), where this is unambiguous):

multirember&Co a lat col
    = col [] []                                , IF lat == [] 

    = multirember&Co a (cdr lat)
         ( newlat seen => 
             col newlat
                 (cons (car lat) seen) )       , IF (car lat) == a

    = multirember&Co a (cdr lat)
         ( newlat seen => 
             col (cons (car lat) newlat)
                 seen )                        , OTHERWISE

Isn't it just self-evident, what this does? :) Not yet? :) Re-writing again with an imagined pattern-matching pseudocode (with guards), we have

multirember&Co  =  g   where
    g a [b, ...lat] col | b == a  =  g a lat ( n s =>  col     n     [b, ...s] )
                        | else    =  g a lat ( n s =>  col [b, ...n]     s     )
    g a []          col           =                    col     []        []

The semantics of pattern matching should be quite obvious: [b, ...lat] matches [1,2,3] where b = 1 and lat = [2,3]. This is thus just a three-cased equation:

  • When the second argument is an empty list, the "collector" function col is fed two empty lists as its two arguments right away;

  • When the second argument's (which is a list) head element is the same as the first argument, the result is the same as that for recursing with the tail of the list, with the amended collector which -- after it will receive its two arguments, n and s, -- will prepend the current head element (which is a) to the s list, and will feed the two lists to this invocation's collector function col;

  • Otherwise, the head element will be prepended to the n list, after n and s are received by the constructed collector, and the both will be fed further into the current collector function.

In other words we're dealing with two results coming back from the recursive call, prepending the head to the second if the head was a, or to the first if it wasn't.

Thus the call

    (g 1 [ 2, 1, 3, 1, 4, 5 ] col)

is the same as (will result in) the call

    (col [ 2, ...[3, ...[4, ...[5, ...[]]]]]
         [    1, ...[1,            ...[]]  ])

i.e.

    (col [ 2,     3,     4,     5          ]
         [    1,     1                     ])

Another way to look at it is that the following is another, equivalent formulation:

multirember&Co a lat col  =  g a lat id id   where
    id      x  =  x              ; identity function  
    (f ∘ g) x  =  f (g x)        ; function composition
    g a [b, ...lat] c d 
               | b == a  =  g a lat  c     (d ∘ (x => cons b x))  ;    (d ∘ {cons b})
               | else    =  g a lat (c ∘ (x => cons b x))   d     ; (c ∘ {cons b})
    g a []          c d  =  col     (c [])                (d [])

and thus

multirember&Co 1 [ 2, 1, 3, 1, 4, 5 ] col 
=
col (((((id ∘ {cons 2}) ∘ {cons 3}) ∘ {cons 4}) ∘ {cons 5}) [])   ; { } is for
    ( ( (id ∘       {cons 1}) ∘ {cons 1}                  ) [])   ;  partial application
=
col     (id   (cons 2     (cons 3     (cons 4    (cons 5   [])))))
        (id         (cons 1     (cons 1                    []) ) )  

which is self-evidently the same thing.

In yet another pseudocode (with list comprehensions), this reveals itself to be

multirember&Co a lat col  
   = col [ b for b in lat if (b /= a) ] 
         [ b for b in lat if (b == a) ] 
   = ( ((n,s) => col n s) ∘ {partition {/= a}} ) lat

except that only one traversal of the list lat is performed (in the original code), efficiently, building the nested chain of lambda functions mimicking the original list structure; which chain is then evaluated to create the two results, passing them to the top-most collector function col.

All this shows us the power of Continuation-Passing Style (which is what this is) to, in effect, create its own function call protocol, here for example passing back two results from each recursive function call, even though normally in lambda calculus a function can only have one result (even if, say, a pair).

Related