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