Context:

I’ve used Emacs off and on for many years at this point. Throughout that time there have been periods where I really leaned in to it and tried to use it for everything, and there have been periods where I only used it for org and/or magit, etc. I’ve learned lots of things about it and I’ve forgotten lots of things about it, but I’ve never been what I would call an “expert” or even a “power user”. So, when I feel like something isn’t working well in Emacs, I almost always default to the assumption that I’m doing something wrong or misunderstanding something, etc.

So, it very well may be that I’m wrong/crazy in my recent conclusion that use-package might not be the ideal abstraction for managing Emacs packages.

With that out of the way, I’ll say that when I first saw use-package, I thought it was amazing. But, in the years that I’ve been using use-package, I never feel like my init file is “right”. Now, I’m starting to think that maybe it’s use-package that’s wrong and not me (insert Simpsons principal Skinner meme).

I don’t know how best to articulate what I mean by use-package being a “wrong abstraction”, but I’ll try by listing some examples and thoughts.

Autoloads

First of all, I feel like the way autoloads are handled with use-package is too mistake-prone. Libraries/packages typically define their own autoloads, but the use-package default is to eagerly load the package. I understand that installing a library via package.el, etc will process the autoloads for us and that manually/locally installed packages get no such benefit.

But, if we’re using use-package to also manage installing the packages for us (:ensure t), then why shouldn’t it know about the autoloads already and automagically imply a :defer t by default?

So, by default, we have to remember to either add :defer t or we have to remember that setting our own hooks, bindings, or commands will create autoloads for us.

I know that you can configure use-package to behave as though :defer t is set by default, but that’s just broken for packages that don’t have any autoloads.

It feels like maybe use-package is doing too many things. Maybe it was actually more correct in the old days to separate the installation, configuration, and actual loading of packages, rather than trying to do all three in one API.

Configuration that depends on multiple packages is ugly/inconsistent

Many packages are fairly standalone, so you can just do,

(use-package foo
    :defer t
    :config
    (setq foo-variable t))

and it’s clean and beautiful. But, sometimes we have configuration that is across multiple packages. A real-world example for me is magit and project.el. Magit actually provides project.el integration, wherein it adds magit commands to the project-switch-commands and the project-prefix-map. That’s great, but it will only run if/when the magit package is loaded.

So, my first guess at using use-package with magit was this,

(use-package magit
    :ensure t
    :defer t
    :config
    (setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1))

which seems reasonable since I know that magit defines its own autoloads. However, I was confused when I’d be using Emacs and the project.el switch choices showed a magit option sometimes.

I eventually realized what was going on and realized that the solution was to immediately load magit,

(use-package magit
    :ensure t
    :config
    (setq magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1))

but that kind of sucks because there’s no reason to load magit before I actually want to use it for anything. So, what we can do instead is to implement the project.el integration ourselves. It’s really just two commands:

(define-key project-prefix-map "m" #'magit-project-status)
(add-to-list 'project-switch-commands '(magit-project-status "Magit") t)

But, the question is: Where do we put these, and when should they be evaluated? I think that just referring to a function symbol doesn’t trigger autoloading, so I believe these configurations should happen after project.el is loaded, and that it doesn’t matter if magit is loaded or not yet.

Since, project.el is built-in to Emacs, it’s probably most reasonable to do that config in the magit use-package form, but what if project.el were another third-party package that had its own use-package form? Would we add the config in the project use-package form, or in the magit use-package form? Or, we could do something clever/hacky,

(use-package emacs
    :after project
    :requires magit
    :config
    (define-key project-prefix-map "m" #'magit-project-status)
    (add-to-list 'project-switch-commands '(magit-project-status "Magit") t))

But, if we do this a lot, then it feels like our init.el is getting just as disorganized as it was before use-package.

Conclusion

This is too rambly already. I think the point is that I’m becoming less convinced that installing/updating packages, loading them, and configuring them at the same time is the right way to think about it.

Obviously, if you know what you’re doing, you can use use-package to great success. But, I think my contention is that I’ve been familiar with Emacs for a long time, I’m a professional software developer, and I still make mistakes when editing my init file. Either I’m a little dim or the tooling here is hard to use correctly.

Am I the only one?

  • nv-elisp@alien.topB
    link
    fedilink
    English
    arrow-up
    2
    ·
    10 months ago

    But, if we’re using use-package to also manage installing the packages for us (:ensure t), then why shouldn’t it know about the autoloads already and automagically imply a :defer t by default?

    Package installation and activation are two seperate concerns. :ensure ensures the package is isntalled if it isn’t already. Users might want to ensure a package is installed and prefer it to be immediately required.

    So, by default, we have to remember to either add :defer t or we have to remember that setting our own hooks, bindings, or commands will create autoloads for us.

    I feel like you’re misunderstanding what autoloads do. Hooks do not autoload anything. The :commands use-package keyword does autolaod commands. This is useful when a package author has not provided the autoload cookie in the package for a command, or when you wish to forgo loading all of the autoloads.

    I know that you can configure use-package to behave as though :defer t is set by default, but that’s just broken for packages that don’t have any autoloads.

    How is it broken? There are other ways to load a package. Namely, require.

    It feels like maybe use-package is doing too many things.

    It only does what you tell it to do via user options and declarations.`

    but that kind of sucks because there’s no reason to load magit before I actually want to use it for anything. So, what we can do instead is to implement the project.el integration ourselves

    Or (use-package project :ensure nil :defer t :config (require magit)) There are multiple ways to set this sort of thing up and use-package (which should have been named use-feature) can be used to configure built-in features.

    Either I’m a little dim or the tooling here is hard to use correctly.

    Third option: You haven’t taken the time to digest the use-package manual and/or expand the macro to see what it’s doing. It’s a DSL. You have to learn it to use it effectively.

    Am I the only one?

    You must be. Otherwise someone would’ve written “use-package alternatives”, which has an almost searchable ring to it.

    Ultimately, organization comes down to the user. Tools like use-package make it easier, but they do not guarantee it.