magnars/dash.el

[Feature request] defun with &rest and &key

ndwarshuis opened this issue · 5 comments

I am thinking of a macro to define a function like this:

(-defun-kw foo (a b &key one (two "2") &rest args)
  (list a b :one one :two two :rest args))

(foo 1 2)                      ; => (1 2 :one nil :two "2" :rest nil)
(foo 1 2 :two "1")             ; => (1 2 :one nil :two "1" :rest nil)
(foo 1 2 :two "1" 'r1 'r2 'r3) ; => (1 2 :one nil :two "1" :rest (r1 r2 r3))
(foo 1 2 'r1 'r2 'r3)          ; => (1 2 :one nil :two "2" :rest (r1 r2 r3))

This behavior is impossible with cl-defun (both the one in Emacs and the official CL version according to the CL-hyperspec). This is because using &rest with &key with cl-defun will simply bind the entire plist consisting of the keyval pairs to the symbol after &rest; it won't interpret "remaining arguments" as non-keyed arguments.

The only way I know to almost do this with core code in emacs is to bind a plist to one of the positional arguments and use &rest after.

I have code for this working in one of my repositories here. Its more complex than what I'm proposing here because it has a predicate checker like cl-defun (I'm not sure I'm keeping that though).

The way it works is that for each function call, anything that is not a positional argument is bound to an intermediate &rest-like symbol, and the list bound to this symbol is partitioned into a plist for the &key arguments and another normal list with whatever is leftover for &rest. The values for each key are then bound to symbols matching the keys without the colon, and the rest argument list is bound to the symbol after &rest in the signature like normal.

I don't think &optional is necessary since &key arguments can function like optional arguments, and adding &optional might not be worth the extra complexity.

Fuco1 commented

First of all, om.el seems like extremly rad package! I started something similar on my own (https://github.com/Fuco1/orgba), targeting more of org not just the elements, but I'm going to drop that part and use your package instead. Great!

As for a -defun, I think the simplest implementation would be

(defmacro -defun (name args &rest body)
  (declare (indent defun))
  `(defalias ',name
     (-lambda ,args ,@body)))


(-defun my-hello (one (&plist 'foo 'bar))
  (message "one %s foo %s bar %s"
           one
           foo
           bar))

(my-hello 1 '(foo "foo" bar "bar"))

However lists don't support &rest so we would need to implement that (there's only the . matcher). Still you would need to write:

(-defun my-hello (one (&plist 'foo 'bar) (&alist :whatever) (&rest args))
  )

A simple preprocessor can be created to wrap parts between keywords into lists, so one can write it without the extra parens as

(-defun my-hello (one &plist 'foo 'bar &alist :whatever &rest args)
  )
(defun preprocess-arguments (args)
  (magic args))

(defmacro -defun (name args &rest body)
  (declare (indent defun))
  `(defalias ',name
     (-lambda ,(preprocess-arguments args) ,@body)))

#268 is relevant as well, at least in name.

@Fuco1 thank you :) As far as -defun were you thinking of how to implement this using dash functions? Sorry if I wasn't clear; I have the implementation working for this already (although it could be simplified) and the OP was meaning to ask if this would be appropriate to include in dash.

Fuco1 commented

I think it is appropriate, but I would rename &key to &plist to correspond with -lambda signature. That's why I mentioned implementing it via -lambda which will keep it consistent (but possibly introduce other problems).

Also there's the version of @alphapapa. The best would be to somehow merge the solutions so each use-case can be expressed.

Ok got it. I can work on a PR and also try and merge the behavior from #268.