Search code examples
lispcommon-lispquoting

How Lisp (Allegro Common Lisp) uses variables in lambda with ' vs #'


I am hoping someone can explain why tests 1-5 work but test 6 does not. I thought that quoting a lambda with ' and using #' in front of a lambda both returned pointers to the function with the only difference being that the #' will compile it first.

(defun test-1 (y)
  (mapcar (lambda (x) (expt x 2))
      '(1 2 3)))

(defun test-2 (y)
  (mapcar (lambda (x) (expt x y))
      '(1 2 3)))

(defun test-3 (y)
  (mapcar #'(lambda (x) (expt x 2))
      '(1 2 3)))

(defun test-4 (y)
  (mapcar #'(lambda (x) (expt x y))
      '(1 2 3)))

(defun test-5 (y)
  (mapcar '(lambda (x) (expt x 2))
      '(1 2 3)))

(defun test-6 (y)
  (mapcar '(lambda (x) (expt x y))
      '(1 2 3)))

I am using the free version of Franz Industries Allegro Common Lisp. The following are the outputs:

(test-1 2)     ; --> (1 4 9)
(test-2 2)     ; --> (1 4 9)
(test-3 2)     ; --> (1 4 9)
(test-4 2)     ; --> (1 4 9)
(test-5 2)     ; --> (1 4 9)
(test-6 2)     ; --> Error: Attempt to take the value of the unbound variable `Y'. [condition type: UNBOUND-VARIABLE]

Solution

  • For a start, you should be aware that your tests 1-4 are conforming Common Lisp, while your tests 5 and 6 are not. I believe Allegro is perfectly well allowed to do what it does for 5 and 6, but what it is doing is outside the standard. The bit of the standard that talks about this is the definition of functions like mapcar, which take function designators as argument, and the definition of a function designator:

    function designator n. a designator for a function; that is, an object that denotes a function and that is one of: a symbol (denoting the function named by that symbol in the global environment), or a function (denoting itself). The consequences are undefined if a symbol is used as a function designator but it does not have a global definition as a function, or it has a global definition as a macro or a special form. [...]

    From this it is clear that a list like (lambda (...) ...) is not a function designator: it's just a list whose car happens to be lambda. What Allegro is doing is noticing that this list is in fact something that can be turned into a function and doing that.

    Well, let's just write a version of mapcar which does what Allegro's does:

    (defun mapcar/coercing (maybe-f &rest lists)
      (apply #'mapcar (coerce maybe-f 'function) lists))
    

    This just uses coerce which is a function which knows how to turn lists like this into functions, among other things. If its argument is already a function, coerce just returns it.

    Now we can write the two tests using this function:

    (defun test-5/coercing (y)
      (mapcar/coercing '(lambda (x) (expt x 2))
                       '(1 2 3)))
    
    (defun test-6/coercing (y)
      (mapcar/coercing '(lambda (x) (expt x y))
                       '(1 2 3)))
    

    So, after that preamble, why can't test-6/explicit work? Well the answer is that Common Lisp is (except for for special variables) lexically scoped. Lexical scope is just a fancy way of saying that the bindings (variables) that are available are exactly and only the bindings you can see by looking at the source of the program. (Except, in the case of CL for special bindings, which I'll ignore, since there are none here.)

    So, given this, think about test-6/coercing, and in particular the call to mapcar/coercing: in that call, coerce has to turn the list (lambda (x) (expt z y)) into a function. So it does that. But the function it returns doesn't bind y and there is no binding for y visible in it: the function uses y 'free'.

    The only way that this could work is if the function that coerce constructs for us were to dynamically look for a binding for y. Well, that's what dynamically-scoped languages do, but CL is not dynamically-scoped.

    Perhaps a way of making this even clearer is to realise that we can lift the function creation right out of the function:

    (defun test-7 (y f)
      (mapcar f '(1 2 3)))
    
    > (test-7 1 (coerce '(lambda (x) (expt x y)) 'function))
    

    It's clear that this can't work in a lexically-scoped language.

    So, then, how do tests 1-4 work?

    Well, firstly there are only actually two tests here. In CL, lambda is a macro and (lambda (...) ...) is entirely equivalent to (function (lambda (...) ...)). And of course #'(lambda (...) ...) is also the same as (function (lambda (...) ...)): it's just a read-macro for it.

    And (function ...) is a magic thing (a special form) which says 'this is a function'. The important thing about function is that it's not a function: it's a deeply magic thing which tells the evaluator (or the compiler) that its argument is the description of a function in the current lexical context, so, for instance in

    (let ((x 1))
      (function (lambda (y) (+ x y))))
    

    The x referred to by the function this creates is the x bound by let. So in your tests 2 and 4 (which are the same):

    (defun test-4 (y)
      (mapcar (function (lambda (x) (expt x y)))
          '(1 2 3)))
    

    The binding of y which the function created refers to is the binding of y which is lexically visible, which is the argument of test-4 itself.