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 original save-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.