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.
Make Flet Great Again
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 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.
Recommend
-
94
-
51
The PHP core functions are a complete mess. With PHP 8 on the horizon, and more competition than ever in the programming ecosystem, this might be a last chan...
-
4
Here’s How They’re Going to Make America Great Again
-
5
Introduction Hello, breathtaking DEV people! 👋 All last week I was refactoring my Go projects. And realized that I was using the same tools to check code quality and safety everywhere. Yes, this should be done from ti...
-
10
CI 优化计划 -- Make CI great again!2022-01-27其实在这个 issue 里面,我已经描述过了。TiDB 的 CI 不稳定,不稳定的来源一个是环境(次要因素),一个是测试 case 本...
-
0
Can Elon Musk Make Twitter Great Again?Plus: The platform’s early days, paying for more CNN, and an unlikely appearance on Fox.Elon Musk, Twitter's latest activist investor, want...
-
4
Do you know you can build flutter apps in Python?😮 Flutter is quite popular in the software development world.Let’s deep dive into the world of building flutter apps with Python!🙂 About FLET Before we begin, what is...
-
3
Support is great. Feedback is even better."As usual, all feedback is SUPER WELCOME 😍Feel free to test this free Google Chrome extension and share your feedback with us.Please, take into account that this is a non profit product / MVP...
-
2
Introduction Python is becoming increasingly popular due to its rich Frameworks and Open S...
-
6
Rejected GitHub Profile Achievements 😵 A collection listing Achievements that were rejected when creating the GitHub Profile Achievements feature. This repository attempts to list them all. Rejected Achievements
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK