nullprogram.com/blog/2017/10/27/
Do you long for the days before Emacs 24.3 when flet
was dynamically
scoped? Well, you probably shouldn’t since there are some very good
reasons lexical scope. But, still, a dynamically scoped flet
is situationally really useful, particularly in unit testing. The good
news is that it’s trivial to get this original behavior back without
relying on deprecated functions nor third-party packages.
But first, what is flet
and what does it mean for it to be
dynamically scoped? The name stands for “function let” (or something
to that effect). It’s a macro to bind named functions within a local
scope, just as let
binds variables within some local scope. It’s
provided by the now-deprecated cl
package.
(require 'cl) ; deprecated!
(defun norm (x y)
(flet ((square (v) (* v v)))
(sqrt (+ (square x) (square y)))))
However, a gotcha here is that square
is visible not just to the body
of norm
but also to any function called directly or indirectly from
the flet
body. That’s dynamic scope.
(flet ((sqrt (v) (/ v 2))) ; close enough
(norm 2 2))
;; -> 4
Note: This works because sqrt
hasn’t (yet?) been assigned a bytecode
opcode. One weakness with flet
is that, due to being dynamically
scoped, it is unable to define or override functions whose calls
evaporate under byte compilation. For example, addition:
(defun add-with-flet ()
(flet ((+ (&rest _) :override))
(+ 1 2 3)))
(add-with-flet)
;; -> :override
(funcall (byte-compile #'add-with-flet))
;; -> 6
Since +
has its own opcode, the function call is eliminated under
byte-compilation and flet
can’t do its job. This is similar these
same functions being unadvisable.
cl-lib and cl-flet
The cl-lib
package introduced in Emacs 24.3, replacing cl
, adds a
namespace prefix, cl-
, to all of these Common Lisp style functions.
In most cases this was the only change. One exception is cl-flet
,
which has different semantics: It’s lexically scoped, just like in
Common Lisp. Its bindings aren’t visible outside of the cl-flet
body.
(require 'cl-lib)
(cl-flet ((sqrt (v) (/ v 2)))
(norm 2 2))
;; -> 2.8284271247461903
In most cases this is what you actually want. The old flet
subtly
changes the environment for all functions called directly or
indirectly from its body.
Besides being cleaner and less error prone, cl-flet
also doesn’t
have special exceptions for functions with assigned opcodes. At
macro-expansion time it walks the body, taking its action before the
byte-compiler can interfere.
(defun add-with-cl-flet ()
(cl-flet ((+ (&rest _) :override))
(+ 1 2 3)))
(add-with-cl-flet)
;; -> :override
(funcall (byte-compile #'add-with-cl-flet))
;; -> :override
In order for it to work properly, it’s essential that functions are
quoted with sharp-quotes (#'
) so that the macro can tell the
difference between functions and symbols. Just make a general habit of
sharp-quoting functions.
In unit testing, temporarily overriding functions for all of Emacs is
useful, so flet
still has some uses. But it’s deprecated!
Unit testing with flet
Since Emacs can do anything, suppose there is an Emacs package that
makes sandwiches. In this package there’s an interactive function to
set the default sandwich cheese.
(defvar default-cheese 'cheddar)
(defun set-default-cheese (type)
(interactive
(let* ((options '("cheddar" "swiss" "american"))
(input (completing-read "Cheese: " options nil t)))
(when input
(list (intern input)))))
(setf default-cheese type))
Since it’s interactive, it uses completing-read
to prompt the user
for input. A unit test could call this function non-interactively, but
perhaps we’d also like to test the interactive path. The code inside
interactive
occasionally gets messy and may warrant testing. It
would obviously be inconvenient to prompt the user for input during
testing, and it wouldn’t work at all in batch mode (-batch
).
With flet
we can stub out completing-read
just for the unit test:
;;; -*- lexical-binding: t; -*-
(ert-deftest test-set-default-cheese ()
;; protect original with dynamic binding
(let (default-cheese)
;; simulate user entering "american"
(flet ((completing-read (&rest _) "american"))
(call-interactively #'set-default-cheese)
(should (eq 'american default-cheese)))))
Since default-cheese
was defined with defvar
, it will be
dynamically scoped despite let
normally using lexical scope in this
example. Both of the side effects of the tested function — setting a
global variable and prompting the user — are captured using a
combination of let
and flet
.
Since cl-flet
is lexically scoped, it cannot serve this purpose. If
flet
is deprecated and cl-flet
can’t do the job, what’s the right
way to fix it? The answer lies in generalized variables.
cl-letf
What’s really happening inside flet
is it’s globally binding a
function name to a different function, evaluating the body, and
rebinding it back to the original definition when the body completes.
It macro-expands to something like this:
(let ((original (symbol-function 'completing-read)))
(setf (symbol-function 'completing-read)
(lambda (&rest _) "american"))
(unwind-protect
(call-interactively #'set-default-cheese)
(setf (symbol-function 'completing-read) original)))
The unwind-protect
ensures the original function is rebound even if
the body of the call were to fail. This is very much a let
-like
pattern, and I’m using symbol-function
as a generalized variable via
setf
. Is there a generalized variable version of let
?
Yes! It’s called cl-letf
! In this case the f
suffix is analogous
to the f
suffix in setf
. That form above can be reduced to a more
general form:
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) "american")))
(call-interactively #'set-default-cheese))
And that’s the way to reproduce the dynamically scoped behavior of
flet
since Emacs 24.3. There’s nothing complicated about it.
(ert-deftest test-set-default-cheese ()
(let (default-cheese)
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) "american")))
(call-interactively #'set-default-cheese)
(should (eq 'american default-cheese)))))
Keep in mind that this suffers the exact same problem with
bytecode-assigned functions as flet
, and for exactly the same
reasons. If completing-read
were to ever be assigned its own opcode
then cl-letf
would no longer work for this particular example.