Where parallels cross

Interesting bits of life

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!!

Comments