1

This code shows an elisp function (third example) using lexical-let, which is now passe as I understand. How would it be written today with lexical scope now being a quasi-built-in feature of elisp? My first guess is just to drop the lexical- prefix to let, but does that change the rest of code, especially the funcall uses? Also, what are we winning with the lexical scope version over the first example?

1 Answer 1

5

This code shows an elisp function (third example) using lexical-let

This is the (renamed) third example in the linked page:

(eval-when-compile (require 'cl)) ;; for `lexical-let' macro
(defun compose-cl (f g)
  (lexical-let ((f f)
                (g g))
    (lambda (x)
      (funcall f (funcall g x)))))

which is now passe as I understand.

Yes.

How would it be written today with lexical scope now being a quasi-built-in feature of elisp?

It is entirely built-in, just not enabled by default everywhere. Emacs 27 enables it in a few more places by default, such as *scratch*, *ielm*, etc.

My first guess is just to drop the lexical- prefix to let

With lexical-binding, there is no need for lexical-let or let at all, as the local variables f and g are automatically captured by the contained closure:

(defun compose-lex (f g)
  (lambda (x)
    (funcall f (funcall g x))))

but does that change the rest of code, especially the funcall uses?

No; see below.

Also, what are we winning with the lexical scope version over the first example?

This is the (renamed) first example in the linked page:

(defun compose-quoted (f g)
  `(lambda (x) (,f (,g x))))

This works regardless of lexical-binding, but has the disadvantage of being opaque to the interpreter/byte-compiler, which sees only a (back)quoted list, not a function. As Stefan mentions in a comment, you miss out on several important features as a result of this, including evaluation speed and compiler warnings (the byte-compiler can't optimise or analyse quoted lambdas), debugging backtraces, the ability to step through the function with Edebug, etc. Quoting lambdas is therefore frowned upon, and is usually avoidable.

but does that change the rest of code, especially the funcall uses?

Let's see. Compare the (abridged) function slots of the three aforementioned definitions:

(symbol-function #'compose-quoted)
(lambda (f g)
  (list 'lambda '(x)
        (list f (cons g '(x)))))

(symbol-function #'compose-cl)
(lambda (f g)
  (let ((cl-f f)
        (cl-g g))
    (list 'lambda '(&rest cl-rest)
          (list 'apply
                (lambda (g0 f0 x)
                  (funcall (symbol-value f0)
                           (funcall (symbol-value g0) x)))
                (list 'quote cl-g)
                (list 'quote cl-f)
                'cl-rest))))

(symbol-function #'compose-lex)
(closure (t)
         (f g)
  (lambda (x)
    (funcall f (funcall g x))))

and the (abridged) values they return:

(compose-quoted #'1+ #'1-)
'(lambda (x) (1+ (1- x)))

(compose-cl #'1+ #'1-)
'(lambda (&rest cl-rest)
   (apply (lambda (g0 f0 x)
            (funcall (symbol-value f0)
                     (funcall (symbol-value g0) x)))
          'cl-g 'cl-f cl-rest))

(compose-lex #'1+ #'1-)
(closure ((g . 1-)
          (f . 1+)
          t)
         (x)
  (funcall f (funcall g x)))
2
  • I think the printed representation of the different functions doesn't matter that much. Some of the more important differences would be speed, compiler-warnings, interaction with Edebug and with the backtrace-debugger.
    – Stefan
    May 14, 2019 at 13:58
  • @Stefan I only listed the printed representations for demonstrative purposes, as the OP seemed concerned about lexical-binding changing the code somehow. I'll include your examples under the benefits of evaluating lambdas, thanks.
    – Basil
    May 14, 2019 at 14:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.