Migrating Away From Use-Package
I have zero understanding of what happens when my Emacs boots up and loads the extensions, applications and utils I have asked for in my configuration. Why? Because I have written my configuration with use-package, which hides all practical details under a nice, declarative macro language11 Which is great – please don’t get me wrong here! That macro language is absolutely amazing, but it steps in the way of fully understanding things..
I dislike not understanding what is going on, so I’m starting to slowly migrate
my Emacs configuration over to use more basic primitives.22 As basic as is
humanly reasonable, actually. I will prefer things like (define-key global-map
key action)
over (global-set-key key action)
because the latter is just a
thin wrapper over the former, and I – for once – want to expose the
implementation. But since I’ve only known use-package
, bind-key
and its
high level companions, I didn’t really know how to write a low-level Emacs
configuration file.
These were the things I learned.
push
vs add-to-list
This is more of an Emacs Lisp language question than an Emacs configuration question, but it’s caused me some confusion and it does make a difference in many places in your configuration, so I’ll quickly go through it first.
Both push
and add-to-list
are used to add a new element to a list, updating
it in place. The main difference (besides the syntactic convenience of push
)
is that add-to-list
works like a set inclusion operator. In other words, if
the element you are trying to add to the list already exists in the list, it
will not be added again.
This can be a good thing (if duplicates are bad in that particular list) or it
could be a bad thing (if duplicates are okay and the list is very long – then
add-to-list
has to traverse the entire list every time you add something to
it! That’s going to be slow.)
Loading and the Load Path
This is going to be a short section. Loading in Emacs terminology simply means
evaluating an Emacs Lisp file. On the most basic level, you do that with the
load
function. If you call (load "filename")
Emacs will try to read and
evaluate any of the following, in the following order: filename
,
filename.elc
, filename.el
.
As you can guess, it is advantageous to not specify the .el
file extension,
because if you omit it, Emacs will automatically first try to load the compiled
version of the same code!
So where does Emacs look for these files? In whichever directories are specified
by the load-path
variable. It should contain a list of directories to look in,
and entries appearing earlier in the list are given priority.
Features, provide
and require
This is one of those things which you may expect to be complicated, but it quickly turns out that’s not the case. There are three things to understand:
- There is a global variable called
features
, which contains a list of symbols, and this is interpreted by Emacs as a record of which files it has loaded. - When you call
(provide 'symbol)
, Emacs will addsymbol
to thefeatures
list. - When you call
(require 'symbol)
, Emacs first checks ifsymbol
is in thefeatures
list. If it isn’t, Emacs will load try to load a file with the namesymbol.el
.
That’s it. As you can see, a “feature” isn’t really a thing in Emacs, it’s
just about a convention that when you write a file to be loaded, you end that
file with a call to provide
to be nice and tell Emacs that it has successfully
loaded the file and should not do it again, even if the file is require
d
again.
Note that if you byte-compile your Emacs configuration, any require
calls you
have will be executed also during compilation. This is because anything you
require
can introduce new macros, and the compiler needs to know about these
in order to be able to expand them.33 It also helps prevent it from warning
you about variables not being known to exist.
Binding and Unbinding Keys
Keybinds are recorded in maps. The global map contains keys which are active at all times, and then one or more local maps may also be active, and these provide additional bindings (which overshadow the global map in case of collisions). Local maps are often associated with major and minor modes, and activated and deactivated with them.
If you don’t use the bind-key
utilities that ship with use-package
, you’ll
want to bind keys manually. You could do this through the functions
global-set-key
, local-set-key
, global-unset-key
and so on. But these are
just thin wrappers over the fundamental operation: define-key
.
Basic usage is
(define-key map key action)
where using nil
as an action means unbinding the key from the map. The map can
be e.g. global-map
if you want a bind that’s available at all times. Many
modes have their own maps, which are active only when that mode is active.
You can also create your own maps! I have some custom Org binds, for example, which are collected in a single map under the F4 key. You do this by first preparing a symbol to hold a keymap, then you add some keys to it, and then, as a last step, you bind that keymap to a key. I suspect it has to be the last thing you do, because you’re binding the value of the keymap to a key, not the symbol. So any updates to the symbol will be invisible to the bound keymap.
(define-prefix-command 'keymap) (define-key 'keymap key-second action) (define-key global-map key-first 'keymap)
After this, consecutively pressing key-first
and then key-second
will
trigger action
.
Declare Eager Dependencies
If you want to load and configure some dependency immediately when your Emacs configuration is loaded44 Why would you not want to do that? It depends. If the dependency takes a long time to load, you may very well want to postpone it until it is actually needed the first time., you can do that with the interesting combination of
(when (require 'dependency nil 'noerror) configuration-code)
This will attempt to load dependency
, but failing to do so, it will simply
return nil
and not try to perform any configuration, in contrast to throwing
an error message. From what I can tell ,the configuration code will be executed
each time this code is executed, even if dependency
has already been required.
If you think about it, that makes sense, though.
Lazy Dependencies: autoload
, auto-mode-alist
and eval-after-load
Loading a full dependency can take a few milliseconds. When you start to have many dependencies, the milliseconds stack up and your Emacs will boot up slower. On the other hand, there is no reason to load all dependencies when Emacs boots. Many can be deferred and loaded only once they are actually needed.
I should say that all well-designed dependencies do this automatically. When you “load” these dependencies (using the technique in the section above), you don’t actually load their full implementation, you just evaluate a few autoload declarations.
That said, now and then you come across a package that is not as well designed, so you’ll need to set up the proper autoloads manually. You do this through the somewhat weird pattern of,
- Define autoloads for the dependency
- Define triggers for the autoloads (maybe
auto-mode-alist
or keybinds) - Tell Emacs what configuration you want to run after the dependency is loaded
In code, this may look like
(autoload 'command-from-dependency "dependency") (define-key global-map key #'command-from-dependency) (eval-after-load "dependency" list-of-configuration-statements)
Here, we say that if command-from-dependency
is executed and dependency
is
not in the features
list, Emacs should require dependency
. We also say we
want key
to execute command-from-dependency
, so in effect pressing key
will cause dependency
to be loaded.
Then we have some list-of-configuration-statements
we cannot run until after
dependency
has been loaded: in this case, we get to use eval-after-load
which does what it says on the tin: it evaluates the second argument whenever
the first argument is loaded. Note that list-of-configuration-statements
has
to be quoted! Since it’s a regular function argument, it will get evaluated at
call time, and then the result will be evaluated once the dependency is loaded.
Of course, the trigger for the autoloaded command does not need to be a key – it could be anything, including automatically calling a command whenever a file with a particular extension is opened.
(push '("\\.extension\\'" . dependency-command) auto-mode-alist)
Again, keep in mind that it may not be necessary for you to lazy-load well-written modes manually – they should lazy-load themselves at the correct points in time.
eval-and-compile
vs. eval-when-compile
The way I understand this – and I’m not at all sure I have the correct understanding now, since I haven’t had time to dig into it myself – is that some people like to byte-compile their Emacs configuration to decrease load times. Compilation gets tricky in the presence of powerful macros, which Lisp have.
Of the two forms above, eval-when-compile
is probably the simplest, and it is
mostly just an optimisation technique. Anything inside eval-when-compile
will
be executed once during compilation, and then the entire thing will be replaced
with the result of that execution. So if you have a really expensive function,
you could create an eval-when-compile
form that turns it into a lookup table,
for example.
Then we have eval-and-compile
, which is described in the manual as mostly
having “fairly sophisticated” uses. You can probably tell from the name what it
does: it’s a little like eval-when-compile
, except it doesn’t replace the
original body with the result. It keeps the original body verbatim,
in addition to executing it once during compilation. This means that any
functions or values that are defined in the body will be available both during
compilation and later when the code runs. Useful e.g. if you create a macro that
depends on a function to generate the correct code, because then the compiler
can run the function to see which code to generate.