Make Flet Great Again « null program
source link: http://nullprogram.com/blog/2017/10/27/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
October 27, 2017
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
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
(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
norm but also to any function called directly or indirectly from
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
+ has its own opcode, the function call is eliminated under
flet can’t do its job. This is similar these
same functions being unadvisable.
cl-lib and cl-flet
cl-lib package introduced in Emacs 24.3, replacing
cl, adds a
cl-, to all of these Common Lisp style functions.
In most cases this was the only change. One exception is
which has different semantics: It’s lexically scoped, just like in
Common Lisp. Its bindings aren’t visible outside of the
(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
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
In unit testing, temporarily overriding functions for all of Emacs is
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 (
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)))))
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
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.
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)))
unwind-protect ensures the original function is rebound even if
the body of the call were to fail. This is very much a
pattern, and I’m using
symbol-function as a generalized variable via
setf. Is there a generalized variable version of
Yes! It’s called
cl-letf! In this case the
f suffix is analogous
f suffix in
setf. That form above can be reduced to a more
(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
completing-read were to ever be assigned its own opcode
cl-letf would no longer work for this particular example.
Aggregate valuable and interesting links.
Joyk means Joy of geeK