Problem
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
case with org-src
buffers. Much of the tooling comes from ghc-mod
,
intero
, 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.
Buffers
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 switch-buffer
and *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
via org-tangle
.
The way org-babel
does it by creating special buffers that rebind
the 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
simply using 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.
Babel Blocks
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 originalsave-buffer
. - 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
devious extensible advice-add
.
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.
Renaming Files
The approach is hooking to org-src-mode-hook
to call save-buffer
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
vice-versa?
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
save-buffer
after org-edit-src-save
and removing it with
delete-file
after org-edit-src-exit
. Easy enough, but we have a
problem if we advice it head-on.
Local Advice
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
mode, namely 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
literacy.
(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.
Custom Hook
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.
Conclusion
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 ox-haskell
or ob-haskell
has not respected the block
headers to do advanced tangling. More things to hack I guess.