Literate Programming, Programming, Emacs, Clojure

Improving org-babel-clojure

In a previous blog post, I started to play with org-babel-clojure to improve its capabilities such that Clojure gets better integrated into Org-mode for creating notebooks and Literate programs. The first thing I wanted to do is to remove the 20 seconds timeout that was defaulted with the nrepl. That meant that it was not possible to run procedures for longer than 20 seconds before it died with a timeout. Once this was implemented, the next step was to add a new feature to see the underlying process of a code block. Because the nature of my work (extensive work with big datasets), my procedures take time to run (minutes… hours…) and much information [about the process] is output to the terminal. However, in the org-babel-clojure implementation, you had to wait until the code was executed before being able to see the processing. What I did at the time is to add a new :async code block parameter which told org-babel-clojure to output all the output of the nrepl, when it was being processed, in a new window.

That worked like a charm. However, after much interaction with Nicolas Goaziou, one of the core maintainers of Org-mode, it was clear that my implementation was not an asynchronous implementation but really just a live processing output.

At the same time, I did find another major irritant: if an exception was raised in my Clojure code, then nothing was output to Org-mode, it was simply silently dying. The only way to see the exception was to switch to the Clojure major mode (using C-c ') and to rerun the code block.

Here are the two new improvements to my org-babel-clojure implementation:

  1. Rename the :async block parameter to :show-process
  2. Output the exceptions and errors messages with the output and the value results parameter

By renaming to :show-process I remove the ambiguity of the feature. Eventually we should get to a real asynchronous process, but the issue is that it is much more complex than I initially thought and this is a problem being addressed in Org-mode for all backends and not just org-babel-clojure.

Then every exception or error messages returned by nrepl are appended the value or the output returned by the code block. That way, we immediately see that something is going wrong directly within Org-mode.

Here is the latest version of my org-mode-babel implementation:

(defvar nrepl-sync-request-timeout)

(defun org-babel-execute:clojure (body params)
  "Execute a block of Clojure code with Babel. The block can be executed
   synchenously by default or asynchronously with the :show-process parameter"
  (let ((expanded (org-babel-expand-body:clojure body params))
        (sbuffer "*Clojure Show Process Sub Buffer*")
        (show (if (assoc :show-process params) t nil))
        (response (cons 'dict nil))
    (case org-babel-clojure-backend
       (require 'cider)
       (let ((result-params (cdr (assoc :result-params params))))
         ; Check if the user want to run code asynchronously
         (when show
           ; Create a new window with the show output buffer
           (switch-to-buffer-other-window sbuffer)

           ; Run the Clojure code asynchronously in nREPL
            (lambda (resp) 
              (when (member "out" resp)
                ; Print the output of the nREPL in the asyn output buffer
                (princ (nrepl-dict-get resp "out") (get-buffer sbuffer)))
              (when (member "ex" resp)
                ; In case there is an exception, then add it to the output 
                ; buffer as well
                (princ (nrepl-dict-get resp "ex") (get-buffer sbuffer))
                (princ (nrepl-dict-get resp "root-ex") (get-buffer sbuffer)))
              (when (member "err" resp)
                ; In case there is an error, then add it to the output 
                ; buffer as well
                (princ (nrepl-dict-get resp "err") (get-buffer sbuffer)))
              (nrepl--merge response resp)
              ; Update the status of the nREPL output session
              (setq status (nrepl-dict-get response "status")))

           ; Wait until the nREPL code finished to be processed
           (while (not (member "done" status))
             (nrepl-dict-put response "status" (remove "need-input" status))
             (accept-process-output nil 0.01)

           ; Delete the show buffer & window when the processing is finalized
           (let ((wins (get-buffer-window-list sbuffer nil t)))
             (dolist (win wins)
               (delete-window win))
             (kill-buffer sbuffer))

           ; Put the output or the value in the result section of the code block
           (setq result (concat (nrepl-dict-get response 
                                                (if (or 
                                                      (member "output" result-params)
                                                      (member "pp" result-params))
                                (nrepl-dict-get response "ex")
                                (nrepl-dict-get response "root-ex")
                                (nrepl-dict-get response "err"))))
         ; Check if user want to run code synchronously
         (when (not show)
           (setq response (let ((nrepl-sync-request-timeout 
                             expanded (cider-current-connection) 
           (setq result
                  (nrepl-dict-get response (if (or (member "output" result-params)
                                                   (member "pp" result-params))
                  (nrepl-dict-get response "ex")
                  (nrepl-dict-get response "root-ex")
                  (nrepl-dict-get response "err"))))))
        (require 'slime)
          (insert expanded)
          (setq result
                   ,(buffer-substring-no-properties (point-min) (point-max)))
                 (cdr (assoc :package params)))))))
      (org-babel-result-cond (cdr (assoc :result-params params))
        (condition-case nil (org-babel-script-escape result)
          (error result)))))