Search code examples
lispscopinglexical-scopedynamic-scope

Can you explain this LISP function and why issues arise with dynamic vs.lexical scoping?


While reading up on some lisp history here From LISP 1 to LISP 1.5, I came across this function:

(define (testr x p f u)
  (if (p x)
      (f x)
      (if (atom? x)
          (u)
          (testr (cdr x)
                 p
                 f
                 (lambda ()
                   (testr (car x) p f u))))))

According to McCarthy, "The difficulty was that when an inner recursion occurred, the value of car[x] wanted was the outer value, but the inner value was actually used. In modern terminology, lexical scoping was wanted, and dynamic scoping was obtained."

I can't quite figure out what "outer value" and "inner value" he is referring to nor can I see how this function misbehaves when evaluated with dynamical scoping. I could understand if the lambda some how shadowed 'x' but it is a function of zero arguments.

(It was quite difficult to actually find this function as it seems to be missing from the webpage itself. It was only after exploring the images.tex file here:http://www-formal.stanford.edu/jmc/history/lisp/images.tex that I found it).


Solution

  • Let's do it in Lisp, here Common Lisp. In Common Lisp it's easy to switch between dynamic and lexical binding.

    Lexical Scope

    This example uses lexical binding.

    (defun testr (x p f u)
      (if (funcall p x)
          (funcall f x)
          (if (atom x)
              (funcall u)
              (testr (cdr x)
                     p
                     f
                     (lambda ()
                       (testr (car x) p f u))))))
    

    What should the function do? It should find the right most element in nested lists for which P is true.

    CL-USER 36 > (testr '(1 (2 3) 3 (7 6 6))
                        (lambda (y) (and (numberp y) (oddp y)))
                        #'identity
                        nil)
    7
    
    CL-USER 37 > (testr '(1 (2 3) 3 (6 6 6))
                        (lambda (y) (and (numberp y) (oddp y)))
                        #'identity
                        nil)
    3
    

    As you see, the returned values are as expected.

    Dynamic Scope

    If we use dynamic binding, then this happens:

    (defun testr (x p f u)
      (declare (special x p f u))       ; use dynamic binding
      (if (funcall p x)
          (funcall f x)
          (if (atom x)
              (funcall u)
              (testr (cdr x)
                     p
                     f
                     (lambda ()
                       (testr (car x) p f u))))))
    
    CL-USER 38 > (testr '(1 (2 3) 3 (6 6 6))
                        (lambda (y) (and (numberp y) (oddp y)))
                        #'identity
                        nil)
    
    Stack overflow (stack size 15998).
    

    If we define ecar like car, but to signal an error when the item is not a cons:

    (defun ecar (item)
      (if (consp item)
          (car item)
        (error "Item ~a not a cons" item)))
    
    (defun testr (x p f u)
      (declare (special x p f u))
      (if (funcall p x)
          (funcall f x)
          (if (atom x)
              (funcall u)
              (testr (cdr x)
                     p
                     f
                     (lambda ()
                       (testr (ecar x) p f u))))))
    
    CL-USER 52 > (testr '(1 2)
                        (lambda (y)
                          (and (numberp y) (oddp y)))
                        #'identity
                        nil)
    
    Error: Item NIL not a cons
    

    At the end of the list, x is nil and that's not a cons, so (ecar x) signals an error.

    Problem

    (defun testr (x p f u)
      (declare (special x p f u))       ; use dynamic binding
      (if (funcall p x)
          (funcall f x)
          (if (atom x)
              (funcall u)      ; INNER: here the lambda function is called
                               ; with dynamic binding, the value of X
                               ; is the current binding of X from
                               ; the current call.
                               : at the end of a list, X would be NIL.
                               ; Inside the lambda function then X would be NIL, too.
                               ; (car x)  -> returns NIL
                               ; then we are in an endless recursion
    
    
    
                               ; OUTER: with lexical binding, the value
                               ; of X would be the value of some
                               ; binding where the function was
                               ; defined and called earlier.
              (testr (cdr x)              
                     p
                     f
                     (lambda ()            ; our lambda function
                       (testr (car x)      ; the reference to X
                              p f u))))))
    

    Simple tracing

    Let's see how it visits the elements:

    Lexical:

    CL-USER 42 > (testr '(1 (2 3) 4 (6 8 10))
                        (lambda (y)
                          (print (list :test y))
                          (and (numberp y) (oddp y)))
                        #'identity
                        nil)
    
    (:TEST (1 (2 3) 4 (6 8 10))) 
    (:TEST ((2 3) 4 (6 8 10))) 
    (:TEST (4 (6 8 10))) 
    (:TEST ((6 8 10))) 
    (:TEST NIL)             ; it has reached the end of the top list
    (:TEST (6 8 10))        ; it recurses down the rightmost sublist
    (:TEST (8 10)) 
    (:TEST (10)) 
    (:TEST NIL)             ; end of the rightmost sublist
    (:TEST 10)              ; checks the elements of the rightmost sublist           
    (:TEST 8) 
    (:TEST 6) 
    (:TEST 4)               ; back up, next element of the top list
    (:TEST (2 3))           ; next sublist of the top list
    (:TEST (3))            
    (:TEST NIL)             ; end of that sublist
    (:TEST 3)               ; checks right element, found
    3
    

    Dynamic:

    CL-USER 40 > (testr '(1 (2 3) 4 (6 8 10))
                        (lambda (y)
                          (print (list :test y))
                          (and (numberp y) (oddp y)))
                        #'identity
                        nil)
    
    (:TEST (1 (2 3) 4 (6 8 10))) 
    (:TEST ((2 3) 4 (6 8 10))) 
    (:TEST (4 (6 8 10))) 
    (:TEST ((6 8 10)))      
    (:TEST NIL)            ; it reaches the end of the top list
    (:TEST NIL)            ; it goes into the endless recursion
    (:TEST NIL) 
    (:TEST NIL) 
    (:TEST NIL) 
    (:TEST NIL) 
    ...