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
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 tolet
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)))
-
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.– StefanMay 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.– BasilMay 14, 2019 at 14:09