Search code examples
variablesemacsclosureslexical-scope

In Elisp, how to access the value cell of a symbol that's bound locally from a closure?


As in the code below, I define a function to create a closure that accepts one argument, the value of which is expected to be a symbol referring to a variable bound in the context of this closure. In the body of the closure, I use symbol-value to get the symbol's value, but it prompts an error saying Symbol's value as variable is void, I expect evaluating this snippet to show 123.

So here I have two questions:

  1. Why symbol-value doesn't work ?
  2. How do I correct this snippet to get the desired result ?
(defun make-closure ()
  (lexical-let ((var1 123))
    (lambda (name)
      ;; How can I get the value cell of the symbol
      ;; specified by the argument "name" ?
      ;; This doesn't work.
      (message-box (format "%s" (symbol-value name))))))

(let ((closure (make-closure)))
  (funcall closure 'var1))

Updated:

Actually, I got this question when I was writing some toy code to imitate "Object Oriented". At first it was something like this (which is similar to the second code of Stefan's answer):

(defun new-person (initial-name)
  (lexical-let* ((name initial-name)
                 (say-hi (lambda ()
                           (message-box (format "Hi, I'm %s" name))))
                 (change-name (lambda (new-name)
                                (setq name new-name))))
    (lambda (selector &rest args)
      (cond
       ((equal 'say-hi selector) (apply say-hi args))
       ((equal 'change-name selector) (apply change-name args))
       (t (message-box "Message not understood"))))))

(let ((tony (new-person "Tony")))
  (funcall tony 'say-hi)
  (funcall tony 'change-name "John")
  (funcall tony 'say-hi))

But I felt the clauses of "cond" kinda "boilerplate" and I thought it might be possible to use the symbol passed from the argument, so I modified it to the following, which no longer works, but I couldn't figure out why:

(defun new-person (initial-name)
  (lexical-let* ((name initial-name)
                 (say-hi (lambda ()
                           (message-box (format "Hi, I'm %s" name))))
                 (change-name (lambda (new-name)
                                (setq name new-name))))
    (lambda (selector &rest args)
      (apply (symbol-value selector) args))))

So is that to say that we shouldn't use a symbol to reference a lexically-bound variable in a closure like the above, since the names of them at evaluation aren't guranteed to be the same as they're written in the source ?


Solution

  • You've misunderstood a few things here, but the key thing is that the var1 symbol you're passing with (funcall closure 'var1) is not the symbol in the lexical environment in which your lambda function is defined.

    Macro-expanding the lexical-let form will help to clarify. This:

    (lexical-let ((var1 123))
      (lambda (name)
        (message-box (format "%s" (symbol-value name)))))
    

    is expanded along these lines:

    (progn
      (defvar --cl-var1--)
      (let ((--cl-var1-- 123))
        #'(lambda (name)
            (message-box (format "%s" (symbol-value name))))))
    

    Which is to say that the lexical-let macro re-writes the symbol names you specify in the bindings in a non-conflicting manner.

    Note that you haven't actually done anything with that var1 binding. If you had done, we would see additional references to --cl-var1-- in the code.

    When you pass the symbol var1 to this function, you are passing the canonical var1, not --cl-var1-- (or whatever it ended up being in practice).

    This is all as it should be. The nature of lexical binding is that it affects the code written within that scope, and does not affect code outside. The (let ((closure (make-closure))) (funcall closure 'var1)) form is outside, and therefore does not see the lexically-bound var1 at all.


    When it comes to "correcting" the code, I'm rather struggling to figure out where you are trying to go with this, but with my interpretation you wouldn't want a closure at all, because you're looking for dynamic binding rather than lexical binding. e.g.:

    (defun make-func ()
      (lambda (name)
        (message-box (format "%s" (symbol-value name)))))
    
    (let ((func (make-func))
          (var1 123))
      (funcall func 'var1))
    

    Based on the edit to the question, I would suggest reworking the code slightly so that you are not using lexical binding for the values you're trying to match to the function argument. For instance:

    (defun new-person (initial-name)
      (lexical-let*
          ((name initial-name)
           (map (list
                 (cons 'say-hi (lambda ()
                                 (message-box
                                  (format "Hi, I'm %s" name))))
                 (cons 'change-name (lambda (new-name)
                                      (setq name new-name))))))
        (lambda (selector &rest args)
          (apply (cdr (assq selector map)) args))))