magnars / dash.el

A modern list library for Emacs
GNU General Public License v3.0
1.67k stars 137 forks source link

-- macro? #233

Open alphapapa opened 7 years ago

alphapapa commented 7 years ago

Hey, it's me again with another crazy macro idea. :)

(defmacro -- (&rest body)
  `(lambda (it)
     ,@body))

Used like:

(cl-sort '((1 "one")
           (2 "two"))
         #'>
         :key (-- (car it)))

The idea is that it's nicer than writing:

(cl-sort '((1 "one")
           (2 "two"))
         #'>
         :key (lambda (it)
                (car it)))

Not a big deal necessarily, but single-argument lambdas are so common that it's nice to have a macro for it. I've used an it macro that does the same thing, but -- seems to be consistent with dash's scheme.

If -- is too short of a name, --it might also work, or --lambda could be the anaphoric, single-arg version of -lambda.

Thanks.

Fuco1 commented 7 years ago

I think we had this proposed before and for some reason it didn't move forward or was rejected (or maybe the issue is still open?)

I think there is now a discussion about adding this to Emacs Lisp core language, something like those clojure forms %() or so (I don't use clojure).

basil-conto commented 7 years ago

I can't contribute to the discussion of such a macro, but I'd like to note a couple of things:

  1. Technically the anonymous functions in the example are redundant, as they are equivalent to using #'car as the :key.
  2. dash-functional provides the partially-anaphoric (I'm not really familiar with this terminology) lambda macro -cut, though it is less general than the one being proposed here.
alphapapa commented 7 years ago

I think there is now a discussion about adding this to Emacs Lisp core language, something like those clojure forms %() or so (I don't use clojure).

Oh, cool, could you link me to that please? :)

Technically the anonymous functions in the example are redundant, as they are equivalent to using #'car as the :key.

Yes, I chose a simple example, but I was inspired by a more complex example that required a lambda:

(cl-sort org-recent-headings-list #'>
           :key (lambda (it)
                  (frecency-score (cdr it) :get-fn #'plist-get)))

And that doesn't work with -cut:

(cl-sort list #'>
         :key (-cut frecency-score  (cdr <>) :get-fn #'plist-get))

Because -cut returns a lambda that takes no arguments:

(lambda nil (frecency-score (cdr <>) :get-fn (function plist-get)))

But thanks for reminding me about -cut. :)

alphapapa commented 7 years ago

I guess maybe @Fuco1 was referring to this:

https://www.reddit.com/r/emacs/comments/6j3fi0/anonymous_function_macro_in_elisp/ https://www.reddit.com/r/emacs/comments/6yhg4m/anonymous_function_macro_in_elisp/

But if there's a more official discussion... :)

alphapapa commented 7 years ago

Having read through those, here's another function that might be useful. It recursively finds symbols starting with $, sorts them, and makes them the lambda's arg list:

(defmacro -$ (&rest body)
  (cl-labels ((collect-vars
               (&rest forms)
               (cl-loop for form in forms
                        append (cl-loop for atom in form
                                        if (and (symbolp atom)
                                                (string-match (rx bos "$")
                                                              (symbol-name atom)))
                                        collect atom
                                        else if (consp form)
                                        append (collect-vars atom)))))
    `(lambda ,(cl-sort (collect-vars body)
                       #'string<
                       :key #'symbol-name)
       ,@body)))

Used like:

(let ((l '((1 "one" :one) (2 "two" :two))))
  (cl-loop for triple in l
           for (digit word symbol) = triple
           collect (funcall (-$ (list (list $3 $2) $1))
                            digit word symbol)))
;; => (((:one "one") 1) ((:two "two") 2))
slippycheeze commented 6 years ago

I wrote a long comment looking for this, then discovered https://github.com/troyp/fn.el exists, and implements this.

alphapapa commented 6 years ago

Well, that's interesting. I also had no idea that fn.el exists and is in MELPA. However, I think the <> / <1> <2> syntax is awkward, and especially doesn't look good when an argument is next to a comparator like (fn: < <1> <2>) or (fn: > <> 0). I much prefer e.g. (-$ < $1 $2) or (-$ > $ 0).

And I think I prefer the explicit parens version as well, like (-$ (< $1 $2)) or (-$ (> $ 0)), because it preserves the form of the actual function body.

Luis-Henriquez-Perez commented 5 years ago

@alphapapa, I just wanted to point out that instead of using cl-labels and recursively sorting the body of -$, you can just use -flatten and -filter to get the symbols:

(defun --dollar-symbol? (sym)
  (string-prefix-p "$" (symbol-name sym)))

(defmacro -$ (&rest body)
  (let ((args (->> body
                   (-flatten)
                   (-filter #'--dollar-symbol?)
                   (-uniq)
                   (--sort (string< (symbol-name it) (symbol-name other))))))
    `(lambda ,args ,@body)))

An example:

(-$ (+ $one $two $two $fourth))
;; => expands to
(lambda
  ($fourth $one $two)
  (+ $one $two $two $fourth))

Question: what's the reason for sorting the arguments?

I think this can be a pretty convenient macro. The only reservation I have about it is that it means that you need to be careful about using it when you have symbols prefixed by $. For someone like xah lee who always prefixes his symbols with a $, for example, this macro may not be so useful.

alphapapa commented 5 years ago

@Luis-Henriquez-Perez Yes, and you could also use --filter to avoid having a defined --dollar-symbol? function.

Question: what's the reason for sorting the arguments?

The intention was to use digit arguments, not ordinal numbers in English. The idea is that the order in which arguments are used in the lambda doesn't matter, but the order in which arguments are passed to it does matter. And we don't want to have a predefined argument list, which would defeat the purpose of the macro.

I think this can be a pretty convenient macro. The only reservation I have about it is that it means that you need to be careful about using it when you have symbols prefixed by $.

A symbol has to be chosen, because bare digits cannot be used as variable names. You could easily make the symbol prefix an argument to the macro.

Luis-Henriquez-Perez commented 5 years ago

Yes, and you could also use --filter to avoid having a defined --dollar-symbol? function.

True. I did it this way because I think --dollar-symbol? is slightly more readable. However, since dash is not likely to use --dollar-symbol? often, you're right that it's better just to avoid defining it.

The intention was to use digit arguments, not ordinal numbers in English. The idea is that the order in which arguments are used in the lambda doesn't matter, but the order in which arguments are passed to it does matter

Ok, that makes sense. Darn, I was hoping we could use any $name so we could make the the bodies more readable, but then we can't determine the order of arguments.

A symbol has to be chosen, because bare digits cannot be used as variable names.

Definitely. My reservation was towards using a particular symbol.

You could easily make the symbol prefix an argument to the macro.

That is possible, although I would not be fond of entering that every time I use the macro. Maybe the user should just set the symbol with a variable. However, this would have to be customized before the macro is defined.

alphapapa commented 5 years ago

Ok, that makes sense. Darn, I was hoping we could use any $name so we could make the the bodies more readable, but then we can't determine the order of arguments.

And if you're going to specify the argument list, you're only saving the 4 characters between -$ and lambda. Maybe you would like prettify-symbols to replace lambda with the Greek letter. :)

That is possible, although I would not be fond of entering that every time I use the macro. Maybe the user should just set the symbol with a variable. However, this would have to be customized before the macro is defined.

The typical (or Common Lisp-like) thing to do would be to specify $ (or whatever) as the default symbol prefix so it wouldn't need to be specified, but the user could override it with e.g. a keyword argument. However, I don't think this is a significant concern. We're talking about small, anonymous functions using a handful of numbered arguments. If an author really needs a variable named $1 (or should we number from 0? Dijkstra would not approve...), he can probably figure out a solution, or just use lambda.