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

-defun macro to flexibly handle arguments (better than cl-defun)

Open alphapapa opened this issue 6 years ago • 21 comments

Wanting more flexible argument handling, I tried to implement this with pcase, but I couldn't get it to match the plist at the end. It was much easier with -let, however I ran into a problem that I'm not sure how to overcome, or if it's even possible to do so.

(defmacro -defun (name arg-patterns &rest body)
  (declare (indent defun))
  (-let ((body-sym (gensym))
         ((arg-patterns defaults) (cl-loop with patterns with defaults
                                           for pattern = (pop arg-patterns)
                                           until (eq pattern :defaults)
                                           collect pattern into patterns
                                           finally return (list patterns (car arg-patterns)))))
    `(defun ,name (&rest args)
       (let ((,body-sym (lambda ()
                          ,@body))
             ,@defaults)
         (or ,@(--map `(-when-let (,it args) (funcall ,body-sym))
                      arg-patterns)
             (user-error ,(concat (symbol-name name) ": Called with invalid args: %s") args))))))

(-defun nuts ((type number &keys :salted salted)
              (type &keys :salted salted)
              (type number)
              (type)
              :defaults ((number 10) (salted t)))
  (list :type type :number number :salted salted))

(nuts 'peanuts)  ;; => (:type peanuts :number 10 :salted t)
(nuts 'peanuts 20)  ;; => (:type peanuts :number 20 :salted t)
(nuts 'peanuts 20 :salted t)  ;; => (:type peanuts :number 20 :salted t)

That all works great, but when I try to set :salted to nil, it doesn't work, because -let doesn't match the plist key's nil value:

(nuts 'peanuts :salted nil)  ;; => (:type peanuts :number 10 :salted t)

If this could be fixed, this could be a really useful way to define functions that accept arguments. It would allow one to avoid manually looping over argument lists.

Any ideas would be appreciated. :)

In case anyone is interested, here's my aborted attempt using pcase:

(defmacro pcase-lambda* (arg-patterns &rest body)
  (declare (indent defun))
  (let ((body-sym (gensym)))
    `(lambda (&rest args)
       (let ((,body-sym (lambda ()
                          ,@body)))
         (pcase args
           ,@(--map `(,it (funcall ,body-sym))
                    arg-patterns)
           (_ (user-error "Called with invalid args")))))))

(setq argh (pcase-lambda* (`(,files ,pred . ,(map something)))
             ;; TODO: default values
             (list :files files :pred pred :something something)))

(funcall argh 'files 'pred 'something 'something)  ;; => (:files files :pred pred :something nil)

alphapapa avatar Jul 10 '18 22:07 alphapapa