2

I'm using a closure to keep track of some stuff to do with state; I want to be able to ‘include’ a function into that closure, so it can access the state. The only way this is possible is by creating that function inside the closure.

My idea is to have a function include, itself created inside that lexical environment, which takes a quoted lambda form, and then returns the actual function (which will have been created inside the closure).

Given that lambda is actually a macro which expands to another macro call

(function (lambda ...))

my best attempt has been

;;; -*- lexical-binding: t; -*-

(let ((x 0)) ;; to represent some state variable
  (defun include (quoted-lambda)
    (eval
     (macroexpand quoted-lambda)
     t))) ;; last `t' is to eval with lexical binding

and then

(defalias 'incx (include '(lambda ()
                            (incf x))))

(incx) ;; should get 1

but this just results in (void-variable x)!

How can this be fixed?

Drew
  • 75,699
  • 9
  • 109
  • 225
digitalis_
  • 427
  • 4
  • 10
  • Did you put a local-variable declaration for `lexical-binding` at the top of the containing file? That's needed, to get proper `lexical-binding` behavior. – Drew Oct 30 '16 at 16:39
  • @Drew; I did put the `-*- lexical-binding: t; -*-` in; does `eval-buffer` take note of it? (That's what I was using to run the thing.) **EDIT:** yup, `eval-buffer` is affected by that header, so that doesn't seem to be the problem. – digitalis_ Oct 30 '16 at 16:51
  • No: `eval-buffer` does not look at the header: it's `find-file` (and its ilks) which does. Along the same lines the `lambda` macro does not expands to another macro call `function` is not a macro but a special form). – Stefan Oct 31 '16 at 12:47
  • @Stefan; fixed `function` misunderstanding in question. The point about the header was just to make sure that lexical binding was on, but thank you for the correction. – digitalis_ Oct 31 '16 at 15:03

3 Answers3

6

You're trying here to access the lexical variable x from outside its lexical scope, which obviously can't work. What you can do is pass a lexical environment to eval:

LEXICAL can also be an actual lexical environment, in the form of an alist mapping symbols to their value.

;;; -*- lexical-binding: t; -*-

(defun include (quoted-lambda)
  (eval quoted-lambda
        ;; Using a quoted constant means all expressions returned by
        ;; `include' will share the same environment.  Use `copy-tree'
        ;; to get indenpendent copies.
        '((x . 0))))

;and then

(require 'cl-lib)
(defalias 'incx (include '(lambda ()
                            (cl-incf x))))

(incx) ;; should get 1
npostavs
  • 9,033
  • 1
  • 21
  • 53
  • Nice! Out of interest, do you think there's a cleaner way to do this? (Maybe using EIEIO, or in a different way.) – digitalis_ Oct 30 '16 at 20:52
  • @digitalis_: It's hard to say without seeing more context about what "do this" means. – npostavs Oct 30 '16 at 21:02
  • fair enough! "Do this" means allowing certain functions to reference some state (in my case, it's actually a timer, which is a function that takes either `:start` or `:stop`) privately. – digitalis_ Oct 30 '16 at 22:16
  • 2
    @digitalis_: What's wrong with having all the functions under the same `let`? Or, just using a global variable, it'll probably be easier to debug (the state is always easily inspectable). PS the `macroexpand` is not needed (`eval` already does macroexpansion), I removed it. – npostavs Oct 31 '16 at 03:00
2

What you seem to want is access to the lexical-environment of a function, which would allow something like this

(let ...
  (defun include (lambda-form)
    (eval lambda-form
          (this-lexical-environment))))

But such a function does not exist and while there is a variable internal-interpreter-environment, it's not accessible via Lisp. But let's remember how closures are constructed:

(let ((x 0))
  (lambda () (* x x)))
;; => (closure ((x . 0) t) nil (* x x))

So it seems that we can access the environment of a closure.

(butlast (cadr (let ((x 0))
                 (lambda () (* x x)))))
;; => ((x . 0))

Armed with this knowledge, we can write a function, which injects the environment of one closure into a second one.

(defun lexenv-grant-access (from to)
  "Grant access of FROM's lexenv to TO."
  (let (name env)
    (when (symbolp from)
      (setq from (symbol-function from)))
    (when (symbolp to)
      (setq name to
            to (symbol-function to)))
    (pcase from
      (`(closure ,env-form ,args . ,body)
       (setq env (butlast env-form)))
      (_ (error "FROM is not a closure")))
    (pcase to
      (`(closure ,env-form ,args . ,body)
       (eval
        (if name
            `(defalias ',name (lambda ,args ,@body))
          `(lambda ,args ,@body))
        (append (butlast env-form)
                env)))
      (_ (error "TO is not a closure")))))

(let ((x 0))
  (defun incx ()
    (cl-incf x)))

(defun decx ()
  (cl-decf x))

(decx)
;; => (wrong-type-argument number-or-marker-p nil)

(lexenv-grant-access 'incx 'decx)
(list (incx) (decx))
;; => (1 0)

So what are the problems ? First, this uses an internal implementation detail, i.e. how closure lists are constructed. But more importantly, it won't work with compiled functions. Such a function has a completely different structure and it may not even store the names of the variables, only their values.

;;Byte compiling the function's file...
(symbol-function 'incx)
;; => #[0 "\300\211\242T\240\207" [(0)] 2]
(disassemble 'incx) ;; => ...

Can you spot the value of x ? Anyway, I don't think you want to do this.

politza
  • 3,316
  • 14
  • 16
1

To "give access" to a variable, you simply have to pass it to the closure:

(let ((x 0))
  (defun include (fun)
    (funcall fun x)))

Of course, in your case, you really want to pass the state itself rather than the state's value. You can do that in the following way, using gv-ref

(let* ((x 0)
       (xref (gv-ref x)))
  (defun include (fun)
    (funcall fun xref)))

(defun incx (xref)
  (cl-incf (gv-deref xref)))

Where gv-ref is like C's & and gv-deref is like C's *.

Of course, you can also do it without gv, as in:

(let ((xref (cons 0 nil)))
  (defun include (fun)
    (funcall fun xref)))

(defun incx (xref)
  (cl-incf (car xref)))
Stefan
  • 26,154
  • 3
  • 46
  • 84