general.el icon indicating copy to clipboard operation
general.el copied to clipboard

Add way to define repeatable commands?

Open alphapapa opened this issue 6 years ago • 23 comments

Hi there @noctuid , I'm a big fan of general, so let me first thank you for it. It makes defining keys in Emacs so simple and pleasant.

I came up with this today: https://github.com/alphapapa/defrepeater.el (Reddit thread). I was preparing to submit it to MELPA, but as someone commented, it reminds of Hydra, and it also seems related to what general does.

Would you be interested in integrating this into general in some way? I'd be glad to contribute a PR. It seems a bit of a shame to make a new package containing just this macro when it would seem to fit nicely in this package. It would be great if we could define the repeating command and bind the key at the same time!

Thanks.

alphapapa avatar Jun 12 '18 21:06 alphapapa

Thanks for mentioning this package. I'd rather provide integration with packages like this than absorb them even if they are simple. I imagine that some people would find the functionality useful without wanting to use general.el, so I think it makes sense to keep it as a separate package. I've had this functionality requested before (#72). Hydra can be used to do this, but your package is much simpler and focused on this specific use case. It would be much easier to integrate it with general, and I think that would be great. @ambihelical defrepeater looks a lot more in line with what you wanted.

If you provided a macro that returned a new, automatically named function, the verbosity would be a little better (and that would work with any key definer):

(general-def key (defrepeater-create def))

As for direct integration with general, a :defrepeater keyword would likely be more convenient:

(general-def key '(def :defrepeater t))
;; or for all definitions
(general-def
  :defrepeater t
  key1 'def1
  key2 'def2)

I'd like to add something like that to general, but it would require reworking the extension system first (e.g. extended definition keywords can't currently alter the definition). I've been planning on doing that anyway though.

noctuid avatar Jun 12 '18 23:06 noctuid

Thanks for mentioning this package. I'd rather provide integration with packages like this than absorb them even if they are simple. I imagine that some people would find the functionality useful without wanting to use general.el, so I think it makes sense to keep it as a separate package.

That makes sense.

Hydra can be used to do this, but your package is much simpler and focused on this specific use case.

I tried implementing the winner-mode example with Hydra, and it worked so well that I wondered if there is still any reason to use defrepeater. I guess it could still be useful, especially if it could be integrated with general.

If you provided a macro that returned a new, automatically named function, the verbosity would be a little better (and that would work with any key definer):

(general-def key (defrepeater-create def))

Oh, you mean instead of:

(defrepeater 'winner-undo-repeat #'winner-undo)

You would like:

(defrepeater #'winner-undo)  ;; => (function winner-undo-repeat)

Or something like that? That's not a bad idea. The first macro I made was define-repeatable-command, which was like defun but created two functions, one to do the body forms and one to repeat it. But I realized that defining only the repeating part is more flexible, and then I decided to imitate defalias.

What if I made the new function name optional? e.g. these would do the same thing:

(defrepeater 'winner-undo-repeat #'winner-undo)
(defrepeater #'winner-undo)

Thanks for your help.

alphapapa avatar Jun 12 '18 23:06 alphapapa

Using hydra is a little more involved, and some people may not need/want its full functionality. I'm fine with implementing repeating in general using either with hydra or with defrepeater. I don't think it matters too much which is used, but @ambihelical did have some annoyances with hydra for this (see #72). If you want to make a PR, I could let you know when the extension system is capable of doing this. It should be pretty easy to add support for then. Otherwise, I'll get around to doing it eventually.

What if I reversed the argument order and made the new function name optional? e.g. these would do the same thing

I think that would be pretty convenient.

noctuid avatar Jun 12 '18 23:06 noctuid

Ok, sounds great. Let me know when it's ready and I'll work on the PR. And I'll go ahead and submit to MELPA soon. Thanks.

alphapapa avatar Jun 13 '18 00:06 alphapapa

BTW, completely off-topic, but I noticed the extensive table of contents in the readme, and thought you might find this new package useful to help customize it: https://github.com/alphapapa/org-make-toc

alphapapa avatar Jun 13 '18 00:06 alphapapa

BTW again, if I may misuse this issue to ask a small question, just curious: where did the awesome logo in the readme come from? :)

alphapapa avatar Jun 13 '18 00:06 alphapapa

BTW, completely off-topic, but I noticed the extensive table of contents in the readme, and thought you might find this new package useful to help customize it: https://github.com/alphapapa/org-make-toc

I'm using toc-org for it right now. I wasn't aware of your package when I started using it but saw you mention it on reddit. I will try it out. Maybe you could provide a comparison in the readme?

BTW again, if I may misuse this issue to ask a small question, just curious: where did the awesome logo in the readme come from? :)

It's taken from the SRPG Advance Wars Days of Ruin.

Ok, sounds great. Let me know when it's ready and I'll work on the PR. And I'll go ahead and submit to MELPA soon. Thanks.

Alright. I fixed the extension system so that it can be used to alter definitions. Basically all you'd need to do is add :repeater to general-rewrite-def-keywords and create a function called general-extended-def-:repeater that created the repeater command and returned a new extended definition plist with :def as the new command. See general-extended-def-:predicate for a basic example of a function that alters the definition and see here for the full documentation on the extension system.

noctuid avatar Jun 13 '18 23:06 noctuid

I'm using toc-org for it right now. I wasn't aware of your package when I started using it but saw you mention it on reddit. I will try it out. Maybe you could provide a comparison in the readme?

I just published it, actually. The primary differences are explained in the "Advanced" section of the readme, although they are not mentioned as a comparison to toc-org:

A document may contain multiple tables of contents. Tables of contents can be customized by setting the TOC property of headings to these values:

all: Include all headings in the file, except ignored headings. children: Include only child headings of this ToC. siblings: Include only sibling headings of this ToC. ignore: Omit a heading from the TOC. ignore-children or 0: Omit a heading’s child headings from the TOC. a number N: Include child headings no more than N levels deep in the TOC.

toc-org is a fine package, and will meet the needs of most users, so you may not need to switch. But if you need some of these features, or if you would prefer to use Org properties instead of tags to control the TOC, you could use org-make-toc instead.

It's taken from the SRPG Advance Wars Days of Ruin.

Cool, looks like a lot of neat character art around that game.

Alright. I fixed the extension system so that it can be used to alter definitions. Basically all you'd need to do is add :repeater to general-rewrite-def-keywords and create a function called general-extended-def-:repeater that created the repeater command and returned a new extended definition plist with :def as the new command. See general-extended-def-:predicate for a basic example of a function that alters the definition and see here for the full documentation on the extension system.

Ok, this may take me a little while... :) Thanks.

alphapapa avatar Jun 14 '18 19:06 alphapapa

toc-org is a fine package, and will meet the needs of most users, so you may not need to switch. But if you need some of these features, or if you would prefer to use Org properties instead of tags to control the TOC, you could use org-make-toc instead.

I will look into whether it would be a better fit for this package's readme when I get the chance.

Ok, this may take me a little while... :) Thanks.

I'd be glad to add support myself if you'd like. Is the syntax of defrepeater finalized?

noctuid avatar Jul 28 '18 12:07 noctuid

I'd be glad to add support myself if you'd like. Is the syntax of defrepeater finalized?

That would be good, if you don't mind. I have several other projects I'm working on, and general's inner workings are more than I feel like diving into right now. :)

I think the syntax is finalized, yes, but the package isn't on MELPA yet. I'll work on that now.

alphapapa avatar Aug 01 '18 19:08 alphapapa

Submitted at https://github.com/melpa/melpa/pull/5665

alphapapa avatar Aug 01 '18 20:08 alphapapa

Okay, I'll add it once the package is on Melpa (general won't have defrepeater as a direct dependency but having it on Melpa makes it easier/possible to test with cask).

I also have a couple of suggestions for defrepeater. defun's return value is technically undefined (and while it is unlikely, it could potentially change, so defrepeater should explicitly return the created function.

Also, while it's unlikely that there will be any problems, you might want to prefix the new command with defrepeater- to avoid the possibility of name clashes.

noctuid avatar Aug 01 '18 20:08 noctuid

defun's return value is technically undefined (and while it is unlikely, it could potentially change, so defrepeater should explicitly return the created function.

I see that the docstring for defun indeed says that, but I'm not sure what the point of returning the function is. The macro is simply a wrapper around defun. defrepeater isn't intended to return a certain value. This is not to say that I won't change it, but is there a need? Were you planning to use its return value? (I guess it would save you from munging the symbol name?)

Also, while it's unlikely that there will be any problems, you might want to prefix the new command with defrepeater- to avoid the possibility of name clashes.

The command is suffixed with -repeat, and the macro ensures that the function is not already defined. I think I prefer that scheme over prefixing the created commands with defrepeater, because the commands do not define repeaters, and using a suffix means that lists of commands sort alphabetically. What do you think?

Thanks for your feedback.

alphapapa avatar Aug 01 '18 22:08 alphapapa

but I'm not sure what the point of returning the function is.

It's useful to immediately bind the created function (and would also simplify implementing a :repeater keyword). For example, (define-key <keymap> <key> (defrepeater <def>)) would break if defun did not return the function. In my macros that create a function, I just have something like #',name at the end. It's kind of ridiculous that the return value isn't guaranteed, but I prefer to be safe.

because the commands do not define repeaters, and using a suffix means that lists of commands sort alphabetically

I definitely agree that the naming scheme makes more sense. I don't like the idea of the automatic naming ever failing though. Also, it would probably be better if there was no user-error if a defrepeater was run twice (e.g. reloading config). Maybe use defrepeater-<command> and make an alias for <command>-repeat if possible? The package could also potentially be renamed so that the prefix makes more sense. Feel free to do whatever you want if you think this is overkill; it is a rare case.

noctuid avatar Aug 01 '18 23:08 noctuid

It's useful to immediately bind the created function (and would also simplify implementing a :repeater keyword). For example, (define-key (defrepeater )) would break if defun did not return the function. In my macros that create a function, I just have something like #',name at the end. It's kind of ridiculous that the return value isn't guaranteed, but I prefer to be safe.

Got it. I'll add the return value. Thanks.

I don't like the idea of the automatic naming ever failing though.

I thought about adding a force argument, but I'm not sure if that's a good UI. I'm not sure if removing the check would be a good idea, though; I feel like defrepeater should be safe, e.g. it should never redefine a function by accident. What do you think?

Also, it would probably be better if there was no user-error if a defrepeater was run twice (e.g. reloading config).

You're probably right. Do you think a message or warn would be better?

Maybe use defrepeater- and make an alias for -repeat if possible?

Hmm...maybe. I feel like the defrepeater prefix should be reserved for functions/macros defined in defrepeater.el, not anything defined by using defrepeater. But I'm not completely sure.

The package could also potentially be renamed so that the prefix makes more sense.

Can you give me an example of what you mean? (repeat is already taken, BTW, it's built-in.)

Thanks.

alphapapa avatar Aug 02 '18 00:08 alphapapa

I agree that functions shouldn't be redefined, but I think that it would be preferable if there was no possibility for collision/no reason to warn or message. Prefixing created functions with the prefix reserved for the package would reasonably ensure that. It's fairly common for function/variable-defining macros to prefix with the package prefix.

As for what prefix to use, I was thinking repeater if it isn't already taken. hydra defines defhydra even though its prefix is hydra-. I don't know if that's considered a poor practice, but it's another possibility. repeater-def isn't that bad either.

If you don't want to change the prefix, I'd think it would be fine to leave it as a warn.

noctuid avatar Aug 02 '18 12:08 noctuid

Ok, I think I'll use warn. I'd prefer to leave the prefix the way it is for now. Thanks for all your feedback.

alphapapa avatar Aug 02 '18 16:08 alphapapa

Now that it's in melpa, I will add a :repeater keyword. Based on the syntax, I was under the mistaken impression that it would support a variable for the first argument. If you use eval instead cadr, this will be possible. Otherwise, I will need to use eval in general (I'm not aware of any alternatives). I think it would be preferable to use eval when the macro expands instead of having to call eval on a backquoted macro. Which would you prefer?

noctuid avatar Aug 31 '18 20:08 noctuid

Hi again,

I understand and share your reluctance to use eval, but I feel like it would be a bad idea for me to eval arguments passed to defrepeater. I feel like it's more acceptable and expected for general to do the eval, since it's intentionally wrapping macros in other macros. Does that make sense? Of course, you have more experience in this area than I do, so I may be mistaken.

Thanks.

alphapapa avatar Sep 01 '18 01:09 alphapapa

I don't think that it is much of an issue for defrepeater to use eval for this. One of us has to use eval with defrepeater in its current form. General using eval isn't really any better and is possibly worse. Eval would end up being called from a function and not a macro. I'd have to do something like this in general:

(eval `(defrepeater ,def))

The issue with this is that the macro won't actually be expanded until the function is called. This isn't a huge deal in this specific case (performance almost certainly won't be an issue), but generally you want to macros to be expandable during compilation. If you find yourself needing to eval a macro (e.g. for the above reason, because you need to use apply, etc.), it would probably be better to use a function.

Since defrepeater wraps defun, it currently needs to be a macro. That said, one potential solution that doesn't involve eval would be to use something like (setf (symbol-function symbol) ...) instead of defun. I need to look at elisp's defun to see if it does anything extra that is necessary, but would you be okay with potentially creating a function version of defrepeater if possible? Generally macro definers have unqouted names, but defrepeater could either be left alone (if you'd prefer) or have a new syntax like (defrepeater winner-undo).

noctuid avatar Sep 01 '18 01:09 noctuid

I don't think that it is much of an issue for defrepeater to use eval for this. One of us has to use eval with defrepeater in its current form. General using eval isn't really any better and is possibly worse. Eval would end up being called from a function and not a macro. I'd have to do something like this in general:

Hmm. Well, I guess I've been thinking of this in terms of use-package keywords, which I think of as wrapping existing, independent code on its own terms. Also, I think defrepeater should use syntax like defalias. From that perspective, I don't think it makes sense for defrepeater to call eval. I think of general as "getting its hands dirty" behind the scenes, doing whatever it takes to make things nice for the user, which could include calling eval.

The issue with this is that the macro won't actually be expanded until the function is called. This isn't a huge deal in this specific case (performance almost certainly won't be an issue), but generally you want to macros to be expandable during compilation. If you find yourself needing to eval a macro (e.g. for the above reason, because you need to use apply, etc.), it would probably be better to use a function.

I guess the issue here is that I don't find myself needing to eval a macro. :) I very much appreciate what general does, and it's not that I'm unwilling to accommodate it to a certain extent, but were it not for general, this wouldn't be an issue. I feel like it's general's "responsibility" to handle this, and that defrepeater should be independent and not use eval unless it needs to for its own sake.

I need to look at elisp's defun to see if it does anything extra that is necessary

From a quick glance at it myself, it essentially wraps (defalias (quote name) (function (lambda ..., handling declare forms, which defrepeater doesn't use. Maybe we could build the defalias call directly. On the other hand, I guess that would make it less forward-compatible with future changes to defun (which are probably unlikely, but still...).

would you be okay with potentially creating a function version of defrepeater if possible?

Maybe, but I feel like I need to understand it better before changing it.

Or did you mean an alternative, coexisting version, rather than replacing the macro version? That would probably be okay, but I want to be sure it's necessary before adding it.

Generally macro definers have unqouted names, but defrepeater could either be left alone (if you'd prefer) or have a new syntax like (defrepeater winner-undo).

I think defrepeater should emulate defalias's syntax, so I'd rather not change that.

Thanks.

alphapapa avatar Sep 01 '18 02:09 alphapapa

should use syntax like defalias

Well defalias does normally use fset. For example, this works:

(defvar var #'foo)
(defalias var #'+)
(foo 1 2)
;; => 3

On the other hand, this won't work with defrepeater.

I guess the issue here is that I don't find myself needing to eval a macro. :) I very much appreciate what general does, and it's not that I'm unwilling to accommodate it to a certain extent, but were it not for general, this wouldn't be an issue.

This is really an issue for any function that wants to call defrepeater with a variable, not just general. Given that defrepeater has syntax like it was function, I don't think it's unreasonable to expect it to evaluate its arguments like a function. For example, if a user had a list of functions they wanted to define/bind repeatable versions for (with dolist or however), they would also have to use eval. Maybe this is unlikely, but it's a possibility.

Or did you mean an alternative, coexisting version, rather than replacing the macro version? That would probably be okay, but I want to be sure it's necessary before adding it.

Yes, that's what I meant. I have no issue with you leaving defrepeater as is. It's not necessary, but a function equivalent would mean that general (or a user) wouldn't need to use eval. I could potentially make a PR if you want. If you'd rather not do this, I'll just use eval.

noctuid avatar Sep 01 '18 02:09 noctuid

Actually, you're correct about defrepeater using eval; my bad. You can't rely on argument values during macro expansion. You can if the variables are defined at expansion time, but that's not the case here.

The only reliable ways to still generate a sanely named function would be to add an additional function definer that generated the name at runtime or for general to use eval to expand defrepeater at runtime. If there was a helper function that dynamically generated the name, defrepeater could expand to it instead if the argument(s) passed in are unqouted. That would allow defrepeater to work the same as defalias for the above example. My apologies again for the initial incorrect suggestion.

I'll make a PR for this soon, or if you'd rather not add this complexity, I can leave defrepeater as-is. I think it would be nice to be able to use variables with defrepeater though.

noctuid avatar Sep 01 '18 10:09 noctuid