Use-Package, or Not!

Use-Package, or Not!

It all started when someone suggested I try Meow instead of Evil. I’ve been historically annoyed by the keybinding conflicts between Evil and Org mode among other things. My Emacs configuration was getting crufty anyways.

I tend to revamp my configuration every 8-10 years, I switched to vertico a few years ago, but my clojure setup is ancient, as is my python configuration. I’ve been writing more C and Elisp recently so I’m not missing too much, or at least I don’t know what I’m missing.

So I started by removing Evil and putting in Meow. I also read a few posts about use-package by some people that I really respect. I’ve always had a strong negative opinion about use-package, but maybe I was wrong. I should give it a chance. On y va!

A complete rewrite.

I started a new configuration and moved things very selectively from my old configuration. I started with my readme.org which already managed my .init.el and a few other things. I let a lot of stuff go. I removed windmove, Evil, Helm, and I cleaned up everything as I went. Each package was put into a use-package expression.

I set the default defer to t (True). I wrote it all in org and tangled it all to a single init.el file.

A Definition language

Use-package is just a small definition language, It seems that its maine purpose is to defer loading of packages so that the startup of Emacs goes faster.

It pretends to be helpful and easier to read. The keywords of :custom, :bind, and :hook are handy, but they often add additional syntax in the form of cons, and also obfuscate what is actually being done, requiring you recall which piece is which and what does it do to the names again?

Most of the rest of the keywords seem to be there just to manage doing chunks of code at the right time so that things can be :defered, or done :after other things.

Some Nice Things ?

I do sort of like :bind and :hook. they are concise. Although I think :hook is too terse.

I suppose :defer is good, but even with a lot of my packages defered I didn’t see a noticable difference for my 250 packages.

I especially did not like :config wrapping around so much code. my org config was huge!

The Pain

The questionable formatting and terseness of the code aside, the worst thing was the dependency hell and making packages wait on each other to load. Defer it, no its needed by X and X is needed by Y and Y is really important to have right away.

I spent quite a bit a time on just getting the packages to load and be happy with each other.

The Relief

I got it all working. My configuration still needed some polishing up, but did I really want to continue with this?

I did a code review. I wasn’t too happy about it.

I fixed some things, I added some settings, bindings, hooks and some small packages.

Another review. No. Use-package is not an impovement. This is not better than what I had.

This configuration was smaller and cleaner but that had nothing to do with use-package. Use-package was getting in my way.

With all this experience, perhaps a week with use-package. I decided that no I deteste use-package. It is coming out!! That decision was followed by a big feeling of relief.

The cleanup.

I decided to go back to my old ways.

Packages, Elpa and Melpa

I use elpa and melpa, and I keep all my packages in a simple list so that I can update, install or do whatever I want with them. My load of what is now ~300 packages takes about 25 seconds on my Orange Pi.

The Architecture

This is the architecture I’ve always had, Ive been doing it this way since the late 90’s. I have some code to ensure everything is compiled, and there is a place for everything.

The configuration directory is much cleaner now, since I cleaned it out in the process and grouped things together by topic. I also have my usual lisp directory for random code. Thats were I also link in Mu and my development repo for EMMS.

I just make sure everything is my load-path.

├── config          - Where the configurations tangle to by topic.
├── config.org      - Configuration for all the packages.
├── early-init.el   - Stuff to do early
├── early-packages  - The package list, management functions.
├── README.org      - Everything except the configs and lisp.
├── init.el
├── lisp            - Code that I write or steal

Requires, Sets, Hooks, and Binds.

The nice thing about this, is your code requires what it needs and its done, I dont need to worry about when something loaded, or what it depends on. I just require what I need where I need it.

Use-package’s :bind and :hook keywords seem nice at first. As I removed use-package, I realized that they really are not that nice. I use General for making key bindings, and it is cleaner and nicer than :bind.

add-hook is really not that bad. Each one practically reads like a sentence. For the occasions where I want to assign a single function to a bunch of hooks that is easily done with a small function.

:custom has no advantage over using setopt or setq as you wish.

How it went.

I was certain I didn’t want anything to do with use-package. As I ripped it out of my code, I was surprised at how reassured I became. Each time the code became simpler, more readable and all worries that something was in the right order, or initialized improperly simply disappeared.

As use-package went away I found peace, tranquility and clean easy to read code.

Some Comparisons.

I kept the configuration that has use-package. I wanted to compare it when I was done. When I look at it now I shudder. – I have since deleted the horror.

:custom vs setopt and setq.

Obviously there are times when setopt should be used, I tend not to, simply because I never have. I’ve become more aware of it, and why I might want to use it. It is virtually identical to :custom. Each take a list of assignments.

One nice thing is that if you would set things in :custom, and then later in :config, that can be done together. Where they go depends more on the readabilty and comprehension of the code than the constructs of use-package.

Corfu has a fairly involved configuration here are the two ‘custom’ sections side by side. I think the setopt reads easier.

(use-package corfu

  ...

  :custom
  ;; Make the popup appear quicker
  (corfu-popupinfo-delay '(0.5 . 0.5))
  ;; Always have the same width
  (corfu-min-width 80)
  (corfu-max-width corfu-min-width)
  (corfu-count 14)
  (corfu-scroll-margin 4)
  ;; Have Corfu wrap around when going up
  (corfu-cycle t)
  (corfu-preselect-first t)
  (corfu-cycle t) ; Allows cycling through candidates
  (corfu-auto t)  ; Enable auto completion
  (corfu-auto-prefix 3) ; Complete with less prefix keys
  (corfu-auto-delay 0.9) ; No delay for completion not recommended.
  (corfu-echo-documentation 0.25) ; Echo docs for current completion option


Heres the setopt block for the same configuration of corfu. It’s just cleaner.

(require 'corfu)

;; Make the popup appear quicker
(setopt corfu-popupinfo-delay '(0.5 . 0.5)
      ;; Always have the same width
      corfu-min-width 80
      corfu-max-width corfu-min-width
      corfu-count 14
      corfu-scroll-margin 4
      ;; Have Corfu wrap around when going up
      corfu-cycle t
      corfu-preselect-first t
      corfu-cycle t ; Allows cycling through candidates
      corfu-auto t ; Enable auto completion
      corfu-auto-prefix 3 ; Complete with less prefix keys
      corfu-auto-delay 0.9 ; No delay for completion not recommended.
      corfu-echo-documentation 0.25) ; Echo docs for current completion option

:Bind vs General’s define-key

Here the difference is also very clear. I much prefer using General to create and manipulate keymaps.

(use-package corfu

  :bind (:map corfu-map
              ;; Match `corfu-quick-complete' keybinding to `avy-goto-line'
              ("s-j" . corfu-quick-complete)
              ("M-p" . corfu-popupinfo-scroll-down)
              ("M-n" . corfu-popupinfo-scroll-up)
              ("M-d" . corfu-popupinfo-toggle))

General is actually easier to read here. It’s just a simple list It doesn’t need cons like :bind does.

(require 'corfu)

(general-define-key
      :keymaps 'corfu-map
      ;; Match `corfu-quick-complete' keybinding to `avy-goto-line'
      "s-j" 'corfu-quick-complete
      "M-p" 'corfu-popupinfo-scroll-down
      "M-n" 'corfu-popupinfo-scroll-up
      "M-d" 'corfu-popupinfo-toggle)

:hook vs Add-hook

This was also very clear to me. :hook always requires to me to think to much about what its doing. add-hook is just clear and simple.

I cannot see how this example for CSV mode is better than the actual code that does the same thing. Add-hook in particular is less complicated and easier to read than the cons required by :hook.

Here are the two configurations for org-auto-tangle. I believe the :hook could just be ‘:hook org’. Still 4 lines vs 3.

(use-package org-auto-tangle
  :hook org-mode
  :config
  (setq org-auto-tangle-default t))

What I like about this is that add-hook just reads almost like english. There is no necessary thought or inference involved. I know exactly what it does. Additionally :config just goes away.

(require 'org-auto-tangle)
(add-hook 'org-mode-hook 'org-auto-tangle-mode)
(setq org-auto-tangle-default t)

Here is a simple example that comes up quite a bit, assigning 2 different functions to the same mode hook.

(use-package csv-mode
  :mode "\\.csv\\'"
  :hook ((csv-mode . csv-align-mode)
         (csv-mode . csv-header-line))
  :init
  (setq csv-comment-start-default nil)
  (setq csv-separators '("," "	" ";" "|")))

Decide for yourself. It seems somewhat equal, but I’m still a fan of less magic in code that I need to be able to read and understand. No keywords. Even if ‘auto-mode-alist is a variable I know by heart. I can read that line, and know it without thought.

(require 'csv-mode)
(add-to-list 'auto-mode-alist '("\\.csv\\'" . csv-mode))

(add-hook 'csv-mode-hook 'csv-align-mode)
(add-hook 'cvs-mode-hook 'csv-header-line)

(setq csv-comment-start-default nil)
(setq csv-separators '("," "	" ";" "|"))

Here smartparens adds it’s mode to a bunch of mode hooks. I suppose its easy enough to read either way. But the inference of smartparens-mode is a bit magical.

I have my own helper function that makes it easy to assign any function/mode to bunch of hooks.

(use-package smartparens

  ...

  :hook (prog-mode text-mode markdown-mode
                   emacs-lisp-mode clojure-mode lisp-mode
                   cider-repl-mode)
(require 'smartparens)

(misc-ext-add-fn-to-hooks
     'smartparens-mode
           '(python-mode-hook
             sh-mode-hook
             esh-mode-hook
             r-mode-hook
             ruby-mode-hook
             emacs-lisp-mode-hook
             cider-repl-mode-hook
             scheme-mode-hook
             clojure-mode-hook))

Simplicity vs Complexity

What I see, and experienced with use-package is that it sacrifices readabilty and creates a lot unecessary complexity for some very unclear benefits. As I rewrote each use-package as a require with some code, I felt real relief. Each new chunk of clean code gave me a breath of fresh air.

Conclusion

For me use-package is not worth whatever it offers. It’s trade-offs are more than what it offers in return. The code is less clear, and the interdependence of packages are harder to manage. The few conviences and syntactic sugar don’t contribute any significant readability, and are often worse to read than straight elisp code. The speed difference is not significant. improving my load speed, from 23 seconds to much less is not much.

I do not like use-package at all, and I will continue to be puzzled as to why people do. I gave it a good chance. I coded an entire emacs configuraton with it. A configuration which I have already wiped from existance.


© 2018-2024. All rights reserved.