Moldable Emacs: make your molds async with ease
Too long; didn't read
Make a mold not block your Emacs by just adding an extra keyword to
the mold definition (that is (me-register-mold :key ... :async ((some-var-that-takes-long ...)))
).
(Last blog of the year: have an amazing 2022!)
The problem
Some computations are long. And we should not be waiting for that.
Rather the computer should be waiting on us. It may happen that molds
take a while to compute their result. For example, some of my work
molds queried Jenkins to gather the running statistics of integration
tests: since that required a chain of API calls, I may wait for
seconds. Not fun! I had a pattern for these cases: present some
placeholder text, run the mold with async.el
and eventually present
the real results. Since that didn't happen often, I had been
reinventing the wheel. Ideally, I wanted an annotation of sort to say:
"this is slow, run it asynchronously".
How difficult would that be?
It is a problem indeed
If we generalize a bit, we could see that this is a cross-cutting concern for molds. Today the issue is synchronicity, but tomorrow? For example, what if we would like to add logs to mold? It may likely be that the implementation is similar for all molds: should we really add logs everywhere by hand?
So the real point is: how could we tell moldable-emacs to run a mold in some special way?
And there is a solution
I described some preparatory work for this in one of my last posts.
The idea is to add the async concern to the mold definition. A mold
definition must have a :then
clause like:
(... (:then (:fn ...)) ...)
What we want for a mold to run asynchronously is:
(... (:then (:fn ... :async ...)) ...)
If the :async
is present we want the mold to replay the pattern I
was writing by hand: create a buffer, put a placeholder text, evaluate
things asynchronously, fill the buffer with the output once is
ready.
Let me show the before and after for the "Image To Text" mold.
Before.
(me-register-mold :key "Image To Text" :docs "Extracts text from the image using `imageclip'." :given (:fn (and (eq major-mode 'image-mode) (executable-find "imgclip"))) :then (:fn (let* ((buffer (buffer-name)) (img (list :img (or (buffer-file-name) buffer))) (_ (me-async-map ;; TODO change this when I implement :async `(lambda (s) (shell-command-to-string (format "imgclip -p '%s' --lang eng" s))) (list (or (buffer-file-name) ;; otherwise store the open image in /tmp for imgclip to work on a file (let ((path (concat "/tmp/" buffer))) (write-region (point-min) (point-max) path) path))) `(lambda (_) (with-current-buffer ,buffername (erase-buffer) (clipboard-yank) (plist-put self :text (buffer-substring-no-properties (point-min) (point-max)))))))) (with-current-buffer buffername (erase-buffer) (setq-local self img) (insert "Loading text from image...")))) ...)
After.
(me-register-mold :key "Image To Text" :docs "Extracts text from the image using `imageclip'." :let ((file-name (buffer-file-name)) (buf-name (buffer-name))) :given (:fn (and (eq major-mode 'image-mode) (executable-find "imgclip"))) :then ( :async ((_ (shell-command-to-string (format "imgclip -p '%s' --lang eng" (or file-name ;; otherwise store the open image in /tmp for imgclip to work on a file (let ((path (concat "/tmp/" buf-name))) (write-region (point-min) (point-max) path) path)))))) :fn (let* ((img (list :img (or (buffer-file-name) (buffer-name))))) (with-current-buffer buffername (erase-buffer) (clipboard-yank) (setq-local self img) (plist-put self :text (buffer-substring-no-properties (point-min) (point-max)))))) ...)
My feeling is that the second is simpler to write. This mold
translates an image to text. In the :then
clause of the "before"
snippet we use me-async-map
to run the image recognition software.
There the placeholder text is "Loading text from image...". The
:then
of the "after" snippet introduces a :async
keyword. This
lets you define bindings that the content of :fn
use. Indeed, :fn
will not run until :async
has returned. However, when :async
is
there, moldable-emacs sets a placeholder buffer for you. And you can
still use your Emacs for other things.
Note: I made it easy to swap between sync and async running. Just
change :async
to :no-async
. That will skip the placeholder text
and just run things synchronously, if you were to need it. It seemed
useful to debug things.
Under the hood I achieve this with an interpreter. If I am trying to
run the :then
clause of a mold, I check what it contains. If it
contains :async
, run the async pattern, if :no-async
just wrap the
bindings in a let, otherwise run :fn
's contents. The function to
look at is me-interpret-then. I obtain the async behavior via
async-let
(I always wanted to use it!).
Ah! One thing to keep in mind is that the :async
block runs in the
*emacs*
buffer set by async. This means that functions influenced by
the buffer context return unexpected things. This is why I needed to
introduce the :let
clause in the "Image to Text" mold for
(buffer-name)
and (buffer-file-name)
.
Anyway, now you can make molds async more easily!
Conclusion
No more worries for making molds speedy! Add some bindings in the
:async
section and you will be done!
Happy async molding and happy 2022!!