Literate programming with Monroe and org-mode

September 21, 2016

I'm starting to like literate programming with org-mode more and more, so I decided to try it in a small Clojure project. I'm aware of Marginalia, but since I'm planning to mix Clojure code with shell scripts and draw some charts, Marginalia won't be much of a help here.

By default, org-mode does not have support for Monroe as REPL backend, and with some elisp bending, we are going to add it.

The Code

The plan is to advise org-babel-execute:clojure function, which is responsible for sending Clojure code chunks and reading responses from REPL. Advising is done with :around option - beside adding new REPL backend, we are going to keep old backends shipped with org-mode.

Here is actual code that will do the work. Maybe the most complicated part is comint-mode setup to route replies from REPL, and this has to be done after Monroe decode and process messages.

;; load necessary code
(require 'org)
(require 'ob-clojure)

;; load necessary languages; if you have this, you can skip it
(org-babel-do-load-languages
  'org-babel-load-languages
  '((clojure . t)
    (emacs-lisp . t)))
  
(defun org-babel-execute:clojure-with-monroe-hook (text)
  "Hook executed when we receive processed message from Monroe."
  (when (and (> (length text) 0)
             (not (string-match monroe-prompt-regexp text)))
    (setq org-babel-execute:clojure-with-monroe-result text)))

(defun org-babel-execute:clojure-with-monroe (body params)
  "Execute a block of Clojure code with Babel using Monroe as backend REPL. Will ask for
nREPL server if there are no connections."
  (let ((expanded (org-babel-expand-body:clojure body params))
        org-babel-execute:clojure-with-monroe-result)
    (require 'monroe)
    (add-hook 'comint-output-filter-functions 'org-babel-execute:clojure-with-monroe-hook)

    ;; try to connect if we don't have running REPL buffer
    (when (not (get-buffer monroe-repl-buffer))
      (call-interactively 'monroe)
      ;; wait a tiny amount of time until Emacs process pending comint messages
      (accept-process-output (get-buffer-process monroe-repl-buffer) 0.5)
      ;; Monroe will create a buffer in place. Quickly restore previous buffer, sending Monroe in background.
      (previous-buffer))

    ;; send code for evaluation
    (with-temp-buffer
      (insert expanded)
      (monroe-eval-buffer))

    ;; again, wait until comint process our hook
    (while (progn
             (accept-process-output (get-buffer-process monroe-repl-buffer) 0.5)
             (null org-babel-execute:clojure-with-monroe-result)))
    (remove-hook 'comint-output-filter-functions 'org-babel-execute:clojure-with-monroe-hook)

    (org-babel-result-cond (cdr (assoc :result-params params))
      org-babel-execute:clojure-with-monroe-result
      (condition-case nil (org-babel-script-escape org-babel-execute:clojure-with-monroe-result)
        (error org-babel-execute:clojure-with-monroe-result)))))

(defun org-babel-execute:clojure-advised (oldfn &rest args)
  "Overriden org-babel-execute:clojure."
  (if (eq 'monroe org-babel-clojure-backend)
    (apply 'org-babel-execute:clojure-with-monroe args)
    (apply oldfn args)))

;; do actual replacement
(advice-add #'org-babel-execute:clojure :around #'org-babel-execute:clojure-advised)

After this, we need to setup proper backend:

(setq org-babel-clojure-backend 'monroe)

and run nREPL server in your terminal, so we can evaluate samples below:

$ lein repl

Sample document

Here is a sample document with few use cases that came up to my mind to see how it works. For those not familiar with org-mode, pressing C-c C-c in BEGIN_SRC/END_SRC blocks will evaluate the code and generate #+RESULTS: chunk with received value.

;; -*- mode: org -*-
#+TITLE: Literate programming in Clojure

* Let's explore some basic Monroe + org-mode usage:

#+BEGIN_SRC clojure
(+ 1 2 3 4)
#+END_SRC

#+RESULTS:
: 10

#+BEGIN_SRC clojure
(defn foo
  "Yet another foo."
  [a b c]
  (+ a b c))

(defn baz
  "Introducing awesome baz function."
  [a]
  (+ a (apply + (range 1 3)) 100))

(* (baz 10) 20)
#+END_SRC

#+RESULTS:
: 2260

So far so good. Printing text will return nil, because output will be shown in REPL.

#+BEGIN_SRC clojure
(println "Look maa, no hands!")
#+END_SRC

#+RESULTS:
: nil

but if you add :results output parameter in header, output will be captured:

#+BEGIN_SRC clojure :results output
(println "Look maa, no hands!")
#+END_SRC

#+RESULTS:
: "Look maa, no hands!\n"

Finally, Incanter charting sample:

#+BEGIN_SRC clojure :results code
(use '(incanter core charts datasets))

(save
  (bar-chart ["a" "b" "c" "d" "e"] [10 20 30 25 20])
  "/tmp/bar-chart.png")

#+END_SRC

#+RESULTS:
: nil

#+CAPTION: A basic bar chart
#+NAME: fig:bar chart
[[/tmp/bar-chart.png]]

Incanter code does not produce any output, nor ob-clojure expander knows about :file parameter, so we have to explicitly specify path to generated chart image.

That's it. Enjoy!