I've been studying Haskell and loving it. In order to facilitate my learning, I wanted to explore it using literate programming via org-babel. Aside from the usual setup of haskell-mode and its associates, I was eager to write the following block.
#+BEGIN_SRC haskell myHead :: [x] -> Maybe x #+END_SRC
Once I went to the coding, there is one significant problem I
encountered: my haskell tooling doesn't work when it has no backing
file. Emacs buffers represent just a file or nothing, such is the
org-src buffers. Much of the tooling comes from
stack and what not but all of them depend on a real file
and when a buffer doesn't have a file, it doesn't work. The obvious
solution is to simply make the virtual file into a real one.
As obvious the solution is, the true error with haskell is that it
does not recognize files without a
.hs extension as such you cannot
just load a haskell source file without it having the correct
extension which is weird. So it is not enough to realize the file but
to add the proper extension as well. Here is the snippet for the impatient.
(defconst fn/haskell-file-extension ".hs" "The de facto haskell file extension.") (defun fn/add-haskell-file-extension (name) "Add the extension of .hs to a file or buffer NAME." (if (string/ends-with name fn/haskell-file-extension) name (concat name fn/haskell-file-extension))) (defvar fn/org-haskell-mode-hook nil "Hook when buffer is haskellized.") (defun fn/haskellize-buffer-file (&optional buffer) "Renames an BUFFER with a .hs extension if it doesn't have one." (interactive) (with-current-buffer (or buffer (current-buffer)) (save-buffer) (lexical-let ((name (buffer-name)) (file-name (buffer-file-name))) (if (not (and file-name (file-exists-p file-name))) (error "Buffer '%s' has no backing file" name) (lexical-let ((haskellized-name (fn/add-haskell-file-extension name)) (haskellized-file-name (fn/add-haskell-file-extension file-name))) (cond ((get-buffer haskellized-name) (error "A buffer named '%s' already exists" haskellized-name)) ((string-equal name haskellized-name) (message "Buffer %s is already haskellized" haskellized-name)) (t (rename-file file-name haskellized-file-name t) (rename-buffer haskellized-name) (set-visited-file-name haskellized-file-name) (set-buffer-modified-p nil) (message "Buffer %s is now haskellized" haskellized-name)))))))) (defun fn/org-haskell-buffer-p (&optional buffer) "Check if BUFFER is an org-haskell buffer." (with-current-buffer (or buffer (current-buffer)) (and (eq major-mode 'haskell-mode) (fboundp 'org-src-edit-buffer-p) (org-src-edit-buffer-p)))) (defun fn/haskellize-org-haskell-buffer (&rest _) "Haskellize org haskell buffer." (when (fn/org-haskell-buffer-p) (fn/haskellize-buffer-file (current-buffer)) (run-hooks 'fn/org-haskell-mode-hook))) (defun fn/save-org-haskell-buffer (&rest _) "Save haskell buffer along with the edit buffer." (when (fn/org-haskell-buffer-p) (save-buffer))) (defun fn/cleanup-org-haskell-buffer (orig-fun &rest args) "Cleanup the org-haskell buffer when exiting the edit buffer." (lexical-let ((org-haskell-file-name (buffer-file-name)) (org-haskell-buffer-p (fn/org-haskell-buffer-p))) (prog1 (apply orig-fun args) (when (and (file-exists-p org-haskell-file-name) org-haskell-buffer-p) (delete-file org-haskell-file-name))))) (add-hook 'org-src-mode-hook #'fn/haskellize-org-haskell-buffer t) (advice-add 'org-edit-src-save :before #'fn/save-org-haskell-buffer) (advice-add 'org-edit-src-exit :around #'fn/cleanup-org-haskell-buffer)
Quite a mouthful for such a simple intent and this might apply to other babel buffers. I want to explore the nuance of being literate in org-mode.
In Emacs, you usually create a buffer associated with a file via
find-file. There are times when all you need is a temporary
scratchpad and you do not want the cost of managing the file system,
such is the purpose of
*scratch* buffer. If I
didn't use the literate style, the book I am reading would create a
clutter and would I remember all the files after I finish with the
book? Literate programming allows me to keep one file and export it
org-babel does it by creating special buffers that rebind
save-buffer command to
org-edit-src-save which updates the
original block region thereby creating no extra files while editing
inside blocks. The case is also true for several libraries such as
magit and helm. Thankfully, Emacs allows us to realize buffers by
save-buffer and it creates a backing file for it.
However, we are left now with managing the realized file as well as changing the file extension of it.
From the problems above, what we want is:
- When opening the block buffer, create a realized file with the correct extension.
- When using the remapped
org-edit-src-save, also save the backing file as with the original
- When closing the block buffer, delete the realized file
The first thing anyone wants to look for when making an extension are
hooks. Sadly, the hooks that are relevant to our goal is only
org-src-mode-hook. You can also add
kill-buffer-hook but it might
be out of its scope. Since we have no hooks, we have to resort to the
After fiddling around with
describe-function, the exit command we
are interested in is
org-edit-src-exit and the entry command is the
org-src-mode-hook. We include
org-edit-src-save to complete the
CRUD life cycle.
The approach is hooking to
org-src-mode-hook to call
at the same time changing the file to add the extension. This is what
fn/haskellize-buffer-file does which I want to give some focus on.
Once we realize the file, there is still the buffer and file
separation: if you rename the file, does it rename the buffer or
If you rename the buffer, it does not change the file associated with;
what is changed is
buffer-name via the
rename-buffer function. If
you rename the file through
rename-file, Emacs only see the old file
missing does creates another one if saved; what needs to be changed
rather is the file association which is done through the function
set-visited-file-name. Emphasizing these lines are:
(rename-file file-name haskellized-file-name t) (rename-buffer haskellized-name) (set-visited-file-name haskellized-file-name)
Aside from the usual file handling, his is the only nuance with the
separation when changing file or buffer names. Now we created realized
and renamed the buffer. Updating and deleting it is as simple calling
org-edit-src-save and removing it with
org-edit-src-exit. Easy enough, but we have a
problem if we advice it head-on.
Since our code depends on a specific mode but advicing does not, if
you add the advice and modify some other code block, it will create
the file unintentionally. Our code needs to run on a specific major
haskell-mode. Wouldn't it be nice to have buffer local
advices like with variables? Since we don't, we add safety by checking
for the major mode.
(with-current-buffer (current-buffer) (eq major-mode 'haskell-mode))
And to be sure the current buffer is a babel buffer, we have
org-src-edit-buffer-p which checks if the buffer is intended for
(with-current-buffer (current-buffer) (org-src-edit-buffer-p))
Combining the two is enough safety. With that, it is enough to get through our intent.
A feature you can add lastly is a custom hook, which is just a list of functions and not some ethereal object such as a kill ring, when the buffer is haskell-ized. Remember how to declare it still reminds me how I need to write more lisp:
(defvar fn/org-haskell-mode-hook (list) "Hook when buffer is haskellized." :type 'hook)
It is just a normal variable but with the type
'hook, not obvious.
What can we do with this new hook, here is what I've done:
(defun fn/haskell-process-load-or-reload () "Invoke reload process without switching buffers" (save-window-excursion (haskell-process-load-or-reload))) (defun fn/haskell-reload-on-save () "Reload interactive haskell process on save." (add-hook 'after-save-hook 'fn/haskell-process-load-or-reload t t)) (defun fn/hindent-before-save () "Reformat before saving." (interactive) (add-hook 'before-save-hook 'hindent-reformat-buffer t t)) (add-hook 'fn/org-haskell-mode-hook 'fn/haskell-process-load-or-reload) (add-hook 'fn/org-haskell-mode-hook 'fn/haskell-reload-on-save) (add-hook 'fn/org-haskell-mode-hook 'fn/hindent-before-save) ;;
What I is to automatically feed the haskell code into the REPL and update it once I saved it and some linting won't hurt as well. The thing is I can't run any of these unless the buffer has an associated haskell file with it. You can think of others.
As usual, this isn't enough to cover all cases but it does it good
enough. There is one other issue I haven't fully resolved which is the
tangling. The book exercises sometimes puts the code into a per
chapter folder which after updating the code must be tangled, the real
issue is how
ob-haskell has not respected the block
headers to do advanced tangling. More things to hack I guess.