lambdalispevalinterpretermetacircular

Alan Kay's Eval/Apply Einstein Moment


Alan Kay said that reading the code closely and finding the 1 and only bug in the code on page 13 of the Lisp 1.5 manual, helped him understand Computer Science by a factor of 100 better.

The code in question is the 1st release of eval & apply that looks anything remotely like modern lisp (that I'm aware of).

Since the correct answer is likely known but lost (my google-fu is decent and I've searched for 20 mins at least) I will award the 1st correct answer (I will be looking at edit times so don't try to cheat) a 250 point bounty As Soon As Possible.

I would suggest others contribute to the bounty as well, remember from the video above Alan Kay said this stuff is reminiscent of the environment Einstein was in when he discovered the Theory of Relativity.

The code in the text is written in M-Expressions. I'm working on a translator to translate from M-expressions to S-expressions (lisp code) @https://github.com/Viruliant/MccarthyMCEval-1.5.

Anyways here is the translated quote from page 13:

;______________________________________Lisp Meta-Circular Evaluator S-Expression
;this code is written in the order it appears on pages 10-13 in the Lisp 1.5 Manual 
;and is translated from the m-expressions into s-expressions

(label mc.equal (lambda (x y)
    (mc.cond
        ((atom x) ((mc.cond ((atom y) (eq x y)) ((quote t) (quote f)))))
        ((equal (car x)(car y)) (equal (cdr x) (cdr y)))
        ((quote t) (quote f)))))

(label mc.subst (lambda (x y z)
    (mc.cond
        ((equal y z) (x))
        ((atom z) (z))
        ((quote t) (cons (subst x y (car z))(subst x y (cdr z)))))))

(label mc.append (lambda (x y)
    (mc.cond 
        ((null x) (y))
        ((quote t) (cons (car x)) (append (cdr x) y)))))

(label mc.member (lambda (x y)
    (mc.cond ((null y) (quote f))
    ((equal x (car y)) (quote t))
    ((quote t) (member x (cdr y))))))

(label mc.pairlis (lambda (x  y  a)
    (mc.cond ((null x) (a)) ((quote t) (cons (cons (car x)(car y))
    (pairlis (cdr x)(cdr y) a)))))

(label mc.assoc (lambda (x a)
    (mc.cond ((equal (caar a) x) (car a)) ((quote t) (assoc x (cdr a))))))

(label mc.sub2 (lambda (a z)
    (mc.cond ((null a) (z)) (((eq (caar a) z)) (cdar a)) ((quote t) (
    sub2 (cdr a) z)))))

(label mc.sublis (lambda (a y)
    (mc.cond ((atom y) (sub2 a y)) ((quote t) (cons (sublis a (car y))))
    (sublis a (cdr y)))))

(label mc.evalquote (lambda (fn x)
    (apply fn x nil)))

(label mc.apply (lambda (fn x a)
    (mc.cond ((atom fn) (
        (mc.cond
            ((eq fn car) (caar x))
            ((eq fn cdr) (cdar x))
            ((eq fn cons) (cons (car x)(cadr x)))
            ((eq fn atom) (atom (car x)))
            ((eq fn eq) (eq (car x)(cadr x)))
            ((quote t) (apply (eval (fn a)x a))))))
        ((eq (car fn) lambda) (eval (caddr fn) (parlis (cadr fn) x a)))
        ((eq (car fn) label) (apply (caddr (fn)x cons (cons (cadr (fn)))
            (caddr fn))a)))))

(label mc.eval (lambda (e a)
    (mc.cond
        ((atom e) (cdr (assoc e a)))
        ((atom (car e)) (mc.cond
            ((eq (car e) quote) (cadr e))
            ((eq (car e) cond) (evcon (cdr e) a))
            ((quote t) (apply (car e) (evlis (cdr e) a) a))))
        ((quote t) (apply (car e) (evlis (cdr e) a) a))))))

(label mc.evcon (lambda (c a)
    (mc.cond 
        ((eval (caar c) a) (eval (cadar c) a))
        ((quote t) (evcon (cdr c) a)))))

(label mc.evlis (lambda (m a)
    (mc.cond
        ((null m) (nil))
        ((quote t) (cons (eval (car m) a) (evlis (cdr m) a)))))))

Solution

  • update2: Here's the code from the paper, rewritten in some pseudocode with list patterns and comprehensions (including parallel comprehensions), in all of 13 lines of code (15 with the addition of FUNARG device, discussed below), all here in one definition:

    apply f args = eval [f, ...[[QUOTE, a] | a <- args]] []  {- McCarthy-LISP-paper    -}
    
    eval e a | atom e    = head [v | [n, v] <- a, n == e] 
             | otherwise = case e of
      [QUOTE,    x]     ->  x 
      [FUNCTION, x]     ->  [FUNARG, x, a]     {- enclose the definitional environment! -}
      [CONS,  x, y]     ->  [eval x a, ...eval y a] 
      [CAR,      x]     ->  head ( eval x a )
      [CDR,      x]     ->  tail ( eval x a )
      [ATOM,     x]     ->  atom ( eval x a ) 
      [EQ,    x, y]     ->  eval x a == eval y a 
      [COND, ...xs]     ->  head [eval c a | [t, c] <- xs, eval t a] 
      [[LAMBDA,p,b], ...xs] -> eval b [...[[n, eval x a] | n <- p | x <- xs], ...a]
      [[LABEL, n,x], ...xs] -> eval [x, ...xs]  [[n, [LABEL, n, x]], ...a]  
      [[FUNARG,f,d], ...xs] -> eval [f, ...[[QUOTE, eval x a] | x <- xs]] d   {- d (sic) -}
      [x,            ...xs] -> eval [eval x a, ...xs] a             {- fixing the bug,   -}
                    {-  eval [eval x a, ...[eval x a | x <- xs]] a  -- args eval'd twice -}
    

    (2021 update:) to support variable length argument lists with (lambda p ....), we add another clause,

      [[LAMBDA,p,b], ...xs] | atom p ->                                {- this one -}
                               eval b [ [p, [eval x a | x <- xs]], ...a]
      [[LAMBDA,p,b], ...xs] -> eval b [...[[n, eval x a] | n <- p | x <- xs], ...a]
    

    where pat | guard -> .... fires when guard evaluates to true when and if pat matches.


    update: here's a video by Philip Wadler where he talks (at 22:30) about that "bug", and how using the "wrong variable" (for the environment to be used in evaluating a lambda-body, a instead of env in the pseudocode that follows) leads to "dynamic binding" rather than "static biding".


    I've re-written the original M-expressions from the code in the book (Lisp 1.5 programmers manual) in some ad-hoc pseudocode which is easier for me to follow, using : for cons or ., with pattern-matching, etc., which follows shortly.

    Actually, I can't see any double-evaluation problem with it. apply expects its arguments already evaluated, and eval evaluates them for it.

    I think the bug was in the original paper "Recursive Functions of Symbolic Expressions" (update: yes! Rainer Joswig mentions at the end of his answer the Paul Graham article, which states that this was in reference to the code in the paper).

    So indeed it looks likely that Alan Kay's remark was in reference to the dynamic scoping. He mentions looking "at the bottom of pg 13" (so, in the book) and that's where the evlis definition is, which evaluates list's elements in the same, current environment. Look at the book's pages 70, 71 for the solution to this, requiring a programmer to explicitly tag their lambda definitions with a new keyword function, to create funarg-tagged lists packaging the lambdas together with (or closing over, as in "closure") the environment at the time the function form is evaluated - i.e. the definitional environment of that lambda form.

    Moses 1970 calls it binding environment, discussing only implicit creation of closures when used as functional arguments in a higher-order function invocation (hence the "funarg" moniker). This is also the context in more contemporary discussions of "deep and shallow binding" (misusing the terminology, in my view) in the word of Perl etc.

    But looking at that extended version in the book, it seems it would allow a programmer to create such entities explicitly, store them in a variable, pass it around as just any other first class entity. Then, when a closure (a funarg-tagged list) is applied, the lambda definition is evaluated in its environment, not the current one (what Moses 1970 calls activation environment). This gets one very close to convincingly imitating lexical scoping with dynamic binding.

    Here's that pseudocode, following the code from the book:

    evalquote fn x = apply fn x []    {- `x` a list of arguments -}
    
    apply CAR ((x:_):_) a = x         {- `a` an association-list, the environment -}
     |    CDR ((_:x):_) _ = x
     |   CONS (x:y:_)   _ = x:y
     |   ATOM (x:_)     _ = atom x
     |     EQ (x:y:_)   _ = eq x y
     | fn xs a , atom fn        = apply (eval fn a) xs a
     | (LAMBDA  args body) xs a = eval body (pairlis args xs a)
     | (LABEL  fn lambda)  xs a = apply lambda xs ((fn:lambda):a)
       {- and the FUNARG device, pg 70: -}
     | (FUNARG lambda env) xs a = apply lambda xs env           {- not `a`, NB! -}
    
    eval e a , atom e      = assv e a
     | (QUOTE e)   _       = e
     | (COND (t c) : cs) a = { eval t a -> eval c a ; eval (COND:cs) a }
       {- the FUNARG device, pg 71: -}
     | (FUNCTION lambda) a = (FUNARG lambda a)  {- enclose the definitional environment! -}
     | (e1:es) a , atom e1 = apply e1 (evlis es a) a 
     | (e1:es) a           = apply e1 (evlis es a) a  
    
    evlis (m : ms) a           = eval m a : evlis ms  | [] _ = []
    equal (x:xs) (y:ys)        = equal x y && equal xs ys
     |    x y , atom x, atom y = eq x y               | _ _  = F
    subst x y z , equal y z = x
     |    _ _ z , atom z    = z
     |    x y (h:t)         = subst x y h : subst x y t
    append (x:xs) ys        = x : append xs ys        | [] ys = ys
    member x (y:_) , equal x y = T 
     |     x (_:ys)         = member x ys             | _ []  = F
    pairlis (x:xs) (y:ys) a = (x:y) : pairlis xs ys a | _ _ a = a
    assv x ((h:y):ys)       = { x==h -> y ; assv x ys }
    sub2 ((x:v):xs) z   = { eq x z -> v ; sub2 xs z } | [] z  = z
    sublis a y , atom y = sub2 a y    | a (y:ys) = sublis a y : sublis a ys