A Handy Emacs Package Configuration Macro

Update April 2015: I now use use-package instead of the with-package macro explained below. It’s cleaner, nicer, and better maintained.

I was inspired by a post recently written by Milkypostman (the M in MELPA). He describes some of his init.el configuration, specifically focusing on an after macro that wraps the misdesigned eval-after-load function. I wanted to take this macro further in three ways:

The result is a pair of macros with-package and with-package* which can be found in package-helper.el. The latter form doesn’t wait but immediately loads the specified packages with require. It’s shaped just like Milkypostman’s after macro, except that it can accept a list of packages in place of a single symbol. Also, the package names aren’t quoted; they don’t need to be since this is a macro instead of a function.

Here’s a typical use case for each macro. That expose higher-order function is from my personal utility library. The expressions to be evaluated depend on both packages and neither needs to be loaded immediately, so I’m using the first form of the macro.

(with-package (skewer-mode utility)
  (define-key skewer-mode-map (kbd "C-c $")
    (expose #'skewer-bower-load "jquery" "1.9.1")))

(with-package* smex
  (global-set-key (kbd "M-x") 'smex))

For the second one, I’m going to be using smex right away (takes over M-x), so I use the second form, which immediately loads smex. The macro isn’t really necessary at all here since I could just use require and follow it with these expressions, but I really like how this organizes my init.el. It creates a domain-specific language (DSL) just for Emacs configuration. Each package configuration is grouped up in a clean let-like form. Since I’ve added syntax highlighting to with-package it looks very elegant. Normal syntax highlighters aren’t going to do this, so here’s a screenshot of my buffer.

JavaScript developers with a keen eye may notice a familiar pattern here. This macro is shaped a bit like the Asynchronous Module Definition (AMD), with asynchronousy in mind. Since this is Lisp with a powerful macro system, I get to hide away the function wrapper part.

Using this macro has caused me to use eval-after-load with just about everything. This has cut my initialization time down to about 10% of what it was before! On those occasions that I do restart Emacs, it’s really nice that it’s back to under 1 second (0.6 seconds vs 6 seconds).

The problem of eval-after-load

I’m calling eval-after-load poorly designed because it’s a perfect example of an inappropriate use of eval. In function form it should have accepted a function as its second argument instead of an s-expression, so it would work like a hook. This is even more inappropriate now that Emacs has proper lexical closures, which is the perfect mechanism for delayed evaluation. The whole point of eval-after-load is to speed up Emacs initialization time, but using eval is slow. To the compiler, this isn’t code, just data. This means no byte-compilation and no compiler warnings.

A possible alternative design for eval-after-load would be a hook named something like <package>-load-hook. Then when load or require loads a file, it runs the hook with the matching name. This removes eval-after-load as its own standalone language concept.

(add-hook 'skewer-mode-load-hook (lambda () ...))

The problem here is when the package is already loaded the hook is never run. In contrast, when eval-after-load is used on an already-loaded package, the expression is immediately evaluated.

Given this, if there was something I could change about this it would simply be for eval-after-load, whatever it would be called, to take a function for the second argument. I would also provide a simple macro just like after that wraps this function. Why not just a macro? The function form would be really useful for a situation like this,

(eval-after-load 'skewer-mode #'skewer-setup)

Here there’s no need to instantiate a new anonymous function or s-expression. If all it’s doing is calling a zero-arity function, that function can be passed in directly.

Have a comment on this article? Start a discussion in my public inbox by sending an email to ~skeeto/public-inbox@lists.sr.ht [mailing list etiquette] , or see existing discussions.

This post has archived comments.

null program

Chris Wellons

wellons@nullprogram.com (PGP)
~skeeto/public-inbox@lists.sr.ht (view)