Moving to literate Emacs configuration.

For a long time, I have been maintaining my emacs configuration through a folder of elisp files. I am quite happy about it and even have it working pretty well on Windows (unfortunately it's my work environment) with help my 3rdparty binaries repository. Finally I decided to move to literate configuration. It's a thing I wanted to do for a long time. Not because it's THE COOL things to do. I do think there is some advantages over my previous approach.

Why

No matter how good written (and obviously not) my elisp code. If I do not constantly read them like I read code for my daily work, there is a good chance I forgot what I did and why I did that way for the configuration, which unfortunately I don't. And I think most people do not read their configuration files as often. It's config files. I may just add a snippet of code at certain point in a hurry to get things working and 3 months later I found it broke something I can't quite remember. Surely you can write a lot of comments in the code, but that will clutter your code as well, make it less readable.

many times, I forgot the packages I installed. Forgot the default keybindings for a packages and couldn't remember what's my long at-hoc lisp function inside a use-package is for. org-mode on the other hand, it's a structured file, designed to read by educated human beings. And you can directly read it on Github. Makes it easier to share with others, I saw many people post their Emacs configuration as a blog post.

How

The killer feature of Emacs is Org mode. One of the feature in it is Babel, which can allow you write and evaluate code snippet directly inside your org files, you can tangle your code snippets, which means to compile them into a generated file. Babel has a special support for Emacs configuration with org-babel-load-file. Your lisp code is automatically generated and loaded for configuration. All you have to do is writing a small snippet in the init file:

;; Load up Org Mode and (now included) Org Babel for elisp embedded in Org Mode files
(setq dotfiles-dir (file-name-directory (or (buffer-file-name) load-file-name)))

;; load up Org and Org-babel
(require 'org)
(require 'ob-tangle)

;; load up all literate org-mode files in this directory
(mapc #'org-babel-load-file (directory-files dotfiles-dir t "\\.org$"))

it will compile each config.org into a config.el[c] and load them. All you have to do is starting the conversion. If you have more than a insignificant collection of elisp config, it will be a long and tedious process.

NOWEB

One advantage over writing lisp directly is NOWEB template, it allows you to break a long configuration snippet into parts for better readability. It works a bit like HTML template libraries like Jinja, not as complex but good for lisp code. One of classic example is breaking down the org templates.

  (setq org-capture-templates
      '(
        <<ORG_CAPTURE>>
       )
  )

Put int misc.org

  ;; misc tasks, moving coding or writing later?
  ("m" "Miscs" entry
               (file+headline ,(my/org-file "miscs.org") "Tasks")
               "* TODO %?\n%i\n  %a" :prepend t)

Capture some ideas in thoughts.org

  ;; my ideas
  ("s" "Thoughts" entry
   (file+headline ,(my/org-file "thoughts.org") "Ideas")
   "* %?\n %i\n \n\n"
   :prepend t)

Something to read.

  ;; Learning items
  ("r" "Reading" entry
   (file+headline ,(my/org-file "reading.org") "Articles")
   "** TODO %?\n%i\n %^L\n \n"
   :prepend t) ;;why the linebreak didn't work?

Each entry of the template can be isolated and you have to admit it's easier to read.

Tangle or load

Once we are done with the configurations. One small optimization is reduce the tangle rate? Evidently you do not need to tangle the org file every single time for loading. Only time it's required is when there is any changes to the org file. Fortunately, it's quite easy to do. We just selectively to tangle based on the file modification-time.

(let* ((dotfile-dir (file-name-directory (or (buffer-file-name)
					     load-file-name)))
       ;; disabled
       ;; (etc-dir   (expand-file-name "etc" dotfile-dir))
       ;; (etc-files (directory-files etc-dir t "\\.org$"))
       (config-org  (expand-file-name "README.org" dotfile-dir))
       (config-el   (expand-file-name "README.el"  dotfile-dir)))
  (require 'org)
  (require 'ob-tangle)
  ;;tangle and load if newer than compiled
  (if (or (not (file-exists-p config-el))
          (file-newer-than-file-p config-org config-el))
      (org-babel-load-file config-org t)
    (load-file config-el)))

Cons

That is about what I want to do for my literate configuration. It is not all sunshine and rainbows though. Like everything, there are pros and cons with this approach. Since we already spend most of the blog post talk about the pros, let's discuss some of the disadvantage over directly loading lisp.

All the cons comes from you are now one layer away from source code. 1. You could potentially sneak in some small bugs undetected. Like unbalanced parenthesis, so be sure to evaluate the code blocks before commit. 2. It's harder and discouraged to write long code in the org file since it defeats the whole purpose, personally I found I will need to convert some of my long configurations into elisp packages (which can be a good thing). 3. You lose some of the helpers like company-mode, paredit-mode when you write code snippet, so naturally you wouldn't write long code.

Summary

Overall, I don't need to write large lisp code for configurations, the use-package already makes configuration much easier. Org-babel sounds like a good match to this use-case.

Happy hacking, folks.

comments powered by Disqus