Literate programming with Monroe and org-mode
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.
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
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!