jwiegley/use-package

recommended way to run code dependent on multiple packages?

unhammer opened this issue · 23 comments

Say I want to run some code as soon as both evil and org are loaded. What's the recommended way to do that?

Should I just use a separate use-package form where I arbitrarily put one as the first argument and the other in :after? Like:

(use-package evil
  :after org
  :config
  (evil-define-key 'normal org-mode-map (kbd "RET") #'org-return))

Or is it better to put a second use-package org (or simply eval-after-load "org") in my main (use-package evil …) form?

Or would it make sense to have a new keyword, e.g.

(use-package evil
  :config
  (evil-set-initial-state 'log-edit-mode 'insert)
  :config-after
  ((foo . (evil-define-key 'normal foo-mode-map (kbd "SPC") #'foo-bork))
   (org . (evil-define-key 'normal org-mode-map (kbd "RET") #'org-return))))

?

I think you want the first form; but I'm not quite clear: what does :config-after do?

It would do the equivalent of putting (eval-after-load 'foo '(evil-define-key …)) (eval-after-load 'org '(evil-define-key …))) inside the main (eval-after-load 'evil …).

Yeah this is a feature I was about to request. Normally if the packages are very logically related, you could just nest it in the dependency's :config section. However, if you depend on more than one package, or if the packages aren't very tightly logically related, it'd be nice to have a way to just say that it should run after an arbitrary number of features becomes available.

I looked at :after but it seems like it runs the configuration as soon as any of the specified features is loaded. I believe it would be nice to be able to specify that it shouldn't load until after all of the features are loaded.

Also the :config-after idea is great, but how about :config-after org and then anything that followed would be the config, just like the :config section. This would be very nice, especially in combination with the ability to wait for multiple features, :config-after (org evil).

vyp commented

@blaenk FWIW John seems to support :add and :any keywords in :after.

@vyp,

(use-package one
  :defer t
  :after two)

macroexpands to

(progn
  (eval-after-load 'two
    '(require 'one)))

ie. it drops the defer for one, so you still need to wrap it either in another use-package or use eval-after-load.

Also the :config-after idea is great, but how about :config-after org and then anything that followed would be the config, just like the :config section. This would be very nice, especially in combination with the ability to wait for multiple features, :config-after (org evil).

I'd love that :-)

Thanks @vyp I wasn't aware of that.

I imagine all we need is a way to process such a DSL, then we can use that same thing to process both :after's argument and a potential :config-after's argument. In the case of :after, it would require the package in the inner body of the nested eval-after-load's like we already do. In the case of the :config-after, it would evaluate the :config-after body in the inner body of the nested eval-after-load's.

  • function/macro to nest multiple eval-after-load's
  • function to process DSL

I'm glad others like the idea of a :config-after section. It would really clean up my init files and make them neat and tidy.

Anyone mind commenting on my eval-after-load-all? It'll take a list of feature symbols and a body, and produces an expression with the body nested inside each of the features.

Input:

(blaenk/eval-after-load-all
  '(evil helm magit)
  '(message "evil, helm, and magit are all loaded")))

Output:

(eval-after-load 'evil
  '(eval-after-load 'helm
     '(eval-after-load 'magit
        '(message "evil, helm, and magit are all loaded"))))

This expression can then be evaluated with eval I guess.

(eval
 (blaenk/eval-after-load-all
  '(evil helm magit)
  '(message "evil, helm, and magit are all loaded")))

This is the first time I do something like this though, so maybe there's a much better way:

(defun blaenk/eval-after-load-all (features body)
  (if (null features)
      body
    (let ((feat (car features))
          (nested (blaenk/eval-after-load-all (cdr features) body)))
      `(eval-after-load (quote ,feat) (quote ,nested)))))

Here's a macro that seems simpler in its implementation and more natural to call, as it removes the need to explicitly quote the arguments or to evaluate them. I'm not sure which is preferable.

(defmacro blaenk/eval-after-load-all (features body)
  (if (null features)
      body
    `(eval-after-load (quote ,(car features))
       (quote (blaenk/eval-after-load-all ,(cdr features) ,body)))))

Input:

(blaenk/eval-after-load-all
  (evil helm magit)
  (message "evil, helm, and magit are all loaded"))

First-level expansion:

(eval-after-load 'evil
  '(blaenk/eval-after-load-all
    (helm magit)
    (message "evil, helm, and magit are all loaded")))

All expanded:

(eval-after-load 'evil
  #'(lambda nil
      (eval-after-load 'helm
        #'(lambda nil
            (eval-after-load 'magit
              #'(lambda nil
                  (message "evil, helm, and magit are all loaded")))))))

I think this is correct? I didn't know that quote expanded to #'(lambda nil inner). Since it ends up wrapping the contents in a lambda, we don't have to worry about the lack of byte compilation? Or do I explicitly have to wrap it in a lambda with (funcall (function ,(lambda () ,@body)))) like this post shows?

I didn't know that quote expanded to #'(lambda nil inner)

It's not quote, it's eval-after-load. This might be surprising since eval-after-load is a function, but it has a compiler-macro that adds the lambda.

Or do I explicitly have to wrap it in a lambda with ``(funcall (function ,(lambda () ,@Body))))` like this post shows?

The explicit wrapping would be needed in 24.3 and earlier.

I don't like the idea of :config-after, it seems like it's exposing too much of the underlying machinery to users. Can you someone please clarify the use case in simple terms? I haven't been following the whole discussion.

Sure. The way I'm imagining it, consider for example that I'm using smartparens and evil, and I want to create bindings for smartparens within evil. Naturally this requires that smartparens is loaded and evil is loaded. One way would be to nest evil's use-package inside of smartparens' :config section. This guarantees that smartparens is loaded by the time evil's configuration runs, but it doesn't feel right because evil doesn't really "depend" on smartparens in that way. What's more, what if there's another package 'foo' that I want to create bindings in evil for, where would I nest evil's use-package then? I've already chosen to nest it in smartparen's :config section.

So another approach is to simply use with-eval-after-load in evil's configuration:

(use-package evil
  :config
  (with-eval-after-load 'smartparens
    ;; my binds
    ))

Sometimes I need to nest a couple of these because a piece of configuration may depend on multiple packages having been loaded, or I simply have many pieces which each depend on a different package:

(use-package evil
  :config
  (with-eval-after-load 'smartparens
    (with-eval-after-load 'other
      ;; config
    )))
  (with-eval-after-load 'a
    ;; config
    ))
  (with-eval-after-load 'b
    ;; config
    ))

So the way I envision :config-after is that instead of having to nest these configurations manually, we could do something like:

(use-package evil
  :config
    ;; general config
  :config-after (:all smartparens other)
    ;; config runs when `smartparens` and `other` features load
  :config-after a
    ;; config runs when `a` feature loads
  :config-after b
    ;; config runs when `b` feature loads

Notice that the :config-after that I envision is a bit different from the one in the OP. Mine looks more like a regular :config statement but takes a feature or list of features to wait for. Also I use the :all keyword to specify that I want to wait until after all of the specified features are loaded, as opposed to :any which would work like how :after works currently for deferring the package loading until any of the specified features are loaded. I only use these features because @vyp pointed out that you've expressed interest in a DSL like this. This would also be added to :after so that we can defer packages until all of the specified features are loaded. I'm using this DSL here with :config-after for consistency.

It's just that I've noticed my configurations (and others I've seen online) often include these kinds of deferred configurations, and it looks like a section like this would make things neat and tidy, making it very clear that those configurations are deferred until the specified features are loaded, instead ot throwing it all in the :config section.

@npostavs That's interesting and I'm glad to have learned that, thanks!

Ok, so what you really want is composition of use-package declarations. :after is a bit different, because it's not composition, it's just dependency.

What if the first symbol to use-package could be a list? In which case it's :config block does not fire until after all those packages have been loaded (and yet, it's :init block would still fire immediately, as with any use-package declaration).

Thus, one might have:

(use-package evil
  :config
  ;; .. stuff just for evil ...
)

(use-package smartparens
  :config
  ;; .. stuff just for smartparens
)

(use-package (evil smartparens)
  :config
  ;; .. stuff that executes at the intersection of evil and smartparens
)

This not only makes the intention very clear, it avoids adding another keyword too.

Yeah I mixed things up. I shouldn't have mentioned :after. I was only relating that :config-after's argument would be the same DSL that will (?) also be passable to :after, based on your comments in #283.

I'm not sure if I personally would adopt that way of doing it. What I absolutely love about your use-package idea is that I can confine for example all of evil's configuration into one use-package evil sexp. I guess it's such a superficial thing but I absolutely love it, and to that end, I would probably just manually use with-eval-after-load in order to keep it all in the same use-package.

The reason I like the idea of :config-after is because it helps to separate things out a bit. I feel like it's much clearer. I can see at a glance that it's also configuration (and so won't run until the package is loaded) but that it also waits on other features to be loaded.

I guess if it's a matter of keyword budgeting, instead of creating an entirely new one via :config-after, would it be possible to add an optional argument to :config? I wonder if that would be impossible because it wouldn't be able to determine if it's the first sexp in the body or if it's the argument, but if it would somehow be possible, this would be the best compromise I think. Something like:

(use-package evil
  :config
    ;; general config
  :config (smartparens other)
    ;; config runs when `smartparens` and `other` features load
  )

Or if it would help in any way in making this possible, perhaps we can make it:

:config (:after smartparens other)

Then we can check if the first sexp has this :after marker, to see if it's the optional argument? Or some variation of this.

One argument in favour of (use-package (evil smartparens) …) is that we avoid multiple identical keywords. That is, with :config-after or :config (:after …), the same use-package form has to be able to have several identical-but-different keywords, which is quite different from how keyword-arguments normally work.

I find the (use-package (list of packages) ...) form most intuitive.

What I absolutely love about your use-package idea is that I can confine for example all of evil's configuration into one use-package evil sexp.

You can still do this:

(use-package evil
  :config
  ...
  (use-package (evil smartparens)
    :config
    ...
  )
)

And yes, all use-package keywords should work equivalently if used with use-package LIST. The only difference (and why I like this approach) is that it extends from expanding to one eval-after-load form, to using several.

Fair enough, I'm convinced. I agree that it's definitely the simplest and most intuitive design.

So what else is there to discuss? As I understand it, this wouldn't obviate adding :all support to the existing :after keyword, correct? That is, (use-package a :after (:all b c) ...), or whatever DSL you end up wanting to use, wouldn't be obviated by (use-package (a b c) ...) because it wouldn't mean the same thing?

I'm fine with the DSL being part of the syntax for :after, since keywords are allowed to define the syntax of what they accept, and adding the DSL would be backwards compatible.

@unhammer @blaenk So, I sat down to implement this today, but it's actually kind of fiendish when you get to the minor details. The issue is, use-package wasn't designed from the beginning to handle package "sets" like this. For example, when establishing an entry into `auto-mode-alist', what happens? The first package is chosen?

I tried two different approaches: rewriting use-package (a b c) into an expanded form. This failed because there are too many details to consider that are only known once expansion is complete. Plus it still runs into the UI complexity issued mentioned in the last paragraph.

Then I tried supporting it directly by changing the code that deals with name to handle lists of names. Again, more complexities started popping up, leading me to realize that this support would generate lots of bugs about corner cases.

With all that said, I'm going to decline adding this, preferring the manual approach when such constructions are necessary:

(use-package evil
  :config
  (evil-set-initial-state 'log-edit-mode 'insert)
  (use-package foo
    :defer t
    :config
    (evil-define-key 'normal foo-mode-map (kbd "SPC") #'foo-bork))
  (use-package org
    :defer t
    :config
    (evil-define-key 'normal org-mode-map (kbd "RET") #'org-return)))

It's much more verbose, which is unfortunate, but it has the advantage of stating very clearly what is meant to happen.

Oh well, thanks for looking into it anyways John!