The art of replacing long lambdas I: llamas, anaphorics, and combinators
Lambdas in Emacs Lisp can look long and noisy. Are there alternatives?
A library for shortening lambdas
Your lambdas can be made short and sweet with llama.
Here's a simple example:
(lambda (str) (upcase (reverse str))) ; regular lambda (##upcase (reverse %)) ; llama
The library can deal with arbitrarily complicated lambdas:
(lambda (foo bar &optional quux &rest more-stuff) (and foo bar (append quux more-stuff))) ; regular lambda (##and % %2 (append &3 &*)) ; llama
At first, llama's syntax will seem weird, but you soon get used to it.
A llama expands to a lambda, which you can then use. So instead of this:
(mapcar (lambda (num) (+ 2 num)) '(39 40)) => '(41 42)
you could write just this:
(mapcar (##+ 2 %) '(39 40)) => '(41 42)
Yet llamas aren't the only thing that can help us.
Anaphoric macros also get rid of long lambdas
Dash users will of course be familiar with this:
(--map (+ 2 it) '(39 40)) => '(41 42)
This is an anaphoric macro. Here, it replaces the lambda with a form, which is much shorter. The lambda would take a single argument, which here is bound to the variable it
. Anaphoric macros are then another way to make code much shorter when lambdas are involved.
You could then ask: with anaphorics, who needs llama?
But you could also ask: with llama, who needs anaphorics?
This in turn makes us ask:
- Are they equally good options?
- Why would you choose one instead of the other?
The answers to these questions are not nearly as straightforward as they might seem. To tackle them, we should first get a better feeling of both llamas and anaphorics in practice.
There's often an anaphoric alternative
Dash is by far the top provider of anaphoric macros for Emacs Lisp, not only in popularity but also in abundance, as it defines more than five dozen of them.
So let's see some examples side by side: the long lambda, the llama, the anaphoric.
The most common cases
It seems that a good two thirds of llamas found in the wild are fed into either mapcar
or mapc
. So let's look at those first.
mapcar
(mapcar (lambda (x) (upcase (symbol-name x))) '(foo bar)) => '("FOO" "BAR") ; lambda (mapcar (##upcase (symbol-name %)) '(foo bar)) => '("FOO" "BAR") ; llama (--map (upcase (symbol-name it)) '(foo bar)) => '("FOO" "BAR") ; dash's anaphoric
- The first one is your regular long lambda.
- The second replaces it with a llama.
- The third is dash's anaphoric.
Here, then, using either a llama or dash's --map
can shorten it by some dozen characters.
mapc
(mapc (lambda (x) (push x results)) '(foo bar)) ; lambda (dolist (x '(foo bar)) (push x results)) ; dolist (uses forms) (--each '(foo bar) (push it results)) ; dash's anaphoric (mapc (##push % results) '(foo bar)) ; llama
Here, using llama gives us the shortest, but using just dolist
is already quite an improvement.
Other simple, common enough cases
A few other functions from native libraries require a lambda and are common enough. Let's see three of them.
mapcan
(mapcan (lambda (x) (when (natnump x) (list x))) '(x 4 y -3 z w 2)) => '(4 2) ; lambda (--mapcat (when (natnump it) (list it)) '(x 4 y -3 z w 2)) => '(4 2) ; dash's anaphoric (mapcan (##when (natnump %) (list %)) '(x 4 y -3 z w 2)) => '(4 2) ; llama
Using llama or dash's anaphoric would shorten this regular mapcan
by at least nine characters.
seq-find
(seq-find (lambda (x) (= 0 (% x 3))) '(1 2 4 5 6 7)) => 6 ; lambda (seq-find (##= 0 (% % 3)) '(1 2 4 5 6 7)) => 6 ; llama ;; ^ ^ a bit confusing (--find (= 0 (% it 3)) '(1 2 4 5 6 7)) => 6 ; dash's anaphoric
Here, using llama shortens this seq-find
by 11 characters, and dash's --find
would remove three more.
The fluctuating difference in length between native+llama versus pure dash's anaphoric boils down to three factors:
- The difference in length between the native function (here
seq-find
) and the anaphoric (here--find
). - The number of times the argument is called, because their names differ in length (e.g.
it
vs.%
). - Whether
##fun
,## fun
, orllama fun
is used. All three are accepted, and the first is recommended.
maphash
So maphash
also needs a lambda, which takes two arguments.
;;; lambda (let ((sum 0)) (maphash (lambda (_k v) (setq sum (+ sum v))) #s(hash-table data (:x 13 :y 14 :z 15))) sum) => 42
Naturally, llama can replace that lambda.
;;; llama (let ((sum 0)) (maphash (##setq sum (+ sum %2)) #s(hash-table data (:x 13 :y 14 :z 15))) sum) => 42
Dash, being mostly about lists, has no anaphoric macro to replace maphash
.
But hash tables are just xht's thing, and its anaphoric h--each
can do the job here:
;;; xht's anaphoric (let ((sum 0)) (h--each (h* :x 13 :y 14 :z 15) (setq sum (+ sum value))) sum) => 42
Yet not all anaphorics are about lambdas
As a side note, not all anaphoric macros are a replacement for lambdas — so llamas cannot be an alternative to these.
Two notable examples are dash's anaphoric threading macros -->
and -some-->
:
(--> 12 (+ 2 it) (mod it 5)) => 4
(-some--> '(foo 4 2 bar) (-remove #'symbolp it) (-remove #'numberp it) (* it it it 10) (/ it 7)) => nil
Sometimes there's no anaphoric alternative
In this case, it would seem as if llama is the only option to get rid of these long lambdas.
But not so fast.
Sometimes dash has non-anaphoric options to avoid long lambdas
dash's -lambda
It seems that -lambda
could also be a "competitor" of llama in some cases where it can be used.
But that would depend on what sort of destructuring is being done.
(mapcar (lambda (x) `(,(car x) ,(cdr x))) '((:a . 1)(:b . 2))) => '((:a 1) (:b 2)) ; lambda (mapcar (-lambda ((a . d)) `(,a ,d)) '((:a . 1) (:b . 2))) => '((:a 1) (:b 2)) ; -lambda (mapcar (##list (car %) (cdr %)) '((:a . 1)(:b . 2))) => '((:a 1) (:b 2)) ; llama
Above, llama and -lambda
have similar readability, but llama wins in size.
And if you're wondering, pcase-lambda
would not help much here:
(mapcar (pcase-lambda (`(,a . ,d)) `(,a ,d)) '((:a . 1) (:b . 2))) => '((:a 1) (:b 2)) ; pcase-lambda
That's both because pcase-lambda
is a lengthy name, and because its destructuring syntax is noisier than -lambda
's.
dash's function combinators
Besides anaphorics, there's another class of "competitors" that llamas must contend with: dash's "function combinators". Though much less well-known than dash's anaphorics, these functions can also get you around regular lambdas, and the resulting expressions are often very readable.
The examples below are sorted by decreasing length, and llamas seem to be slightly longer in most (but not all) of these cases.
-on + -compose
Sometimes sorting destructively is desired, and cl-sort
can do that, while also offering a sorting key — a lambda.
;;; lambda (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (lambda (x) (string-to-number (car x)))) => '(("1" . "2") ("3" . "9") ("5" . "0"))
;;; llama (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (##string-to-number (car %))) => '(("1" . "2") ("3" . "9") ("5" . "0"))
The same could also be accomplished with (non-anaphoric) dash instead of llama. Here we use -compose
:
;;; dash ;;;; -compose (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (-compose #'string-to-number #'car))
Often, non-destructiveness is preferred. For that, there's a dash-only (cl-free) non-anaphoric solution:
;;; dash ;;;; -on + -compose (-sort (-on #'< (-compose #'string-to-number #'car)) '(("5" . "0") ("1" . "2") ("3" . "9"))) => '(("1" . "2") ("3" . "9") ("5" . "0"))
Note that -sort
puts the list as last argument, so it has the additional advantage of being suitable for threading-last (if that's your sort of thing), so you could go on modifying the results. For example:
(->> '(("5" . "0") ("1" . "2") ("3" . "9")) (-sort (-on #'< (-compose #'string-to-number #'car))) (-map #'-value-to-list) -flatten string-join string-to-number) => 123950
Or more efficiently:
(->> '(("5" . "0") ("1" . "2") ("3" . "9")) (-sort (-on #'< (-compose #'string-to-number #'car))) (mapc (-lambda ((a . d)) (princ (concat a d)))) with-output-to-string string-to-number) => 123950
Though if that result is what we wanted all along, we could get there faster and simpler with:
(->> '(("5" . "0") ("1" . "2") ("3" . "9")) (-map (-lambda ((a . d)) (concat a d))) (-sort #'string<) string-join string-to-number) => 123950
Okay, back to dash's combinators.
-partial
When applying-partially, dash is the shortest.
(funcall (lambda (&rest _) (apply #'+ 13 14 _)) 15) => 42 ; lambda (funcall (apply-partially #'+ 13 14) 15) => 42 ; subr.el (funcall (##apply #'+ 13 14 &*) 15) => 42 ; llama (funcall (-partial #'+ 13 14) 15) => 42 ; dash's compiled-fun
Note that combinators produce compiled functions:
(type-of (-partial #'+ 13 14)) => 'compiled-function
-const
Sometimes we want a lambda to return a constant value.
But what to use instead of the lambda depends on what that value is.
A nil
:
(mapcar (lambda (&rest _) nil) '(1 nil 3)) => '(nil nil nil) ; native + lambda (mapcar (##prog1 nil &*) '(1 nil 3)) => '(nil nil nil) ; native + llama (mapcar (##ignore &*) '(1 nil 3)) => '(nil nil nil) ; native + llama (-map (-const nil) '(1 nil 3)) => '(nil nil nil) ; dash + compiled-fun (mapcar #'ignore '(1 nil 3)) => '(nil nil nil) ; native + symbol-fun (-map #'ignore '(1 nil 3)) => '(nil nil nil) ; dash + symbol-fun (--map nil '(1 nil 3)) => '(nil nil nil) ; dash + form (anaphoric)
A t
:
;; Note: #'always was introduced in Emacs 28.1 (mapcar (lambda (&rest _) t) '(1 nil 3)) => '(t t t) ; native + lambda (mapcar (##prog1 t &*) '(1 nil 3)) => '(t t t) ; native + llama (mapcar (##always &*) '(1 nil 3)) => '(t t t) ; native + llama (mapcar #'always '(1 nil 3)) => '(t t t) ; native + symbol-fun (-map (-const t) '(1 nil 3)) => '(t t t) ; dash + compiled-fun (-map #'always '(1 nil 3)) => '(t t t) ; dash + symbol-fun (--map t '(1 nil 3)) => '(t t t) ; dash + form (anaphoric)
A string:
(mapcar (lambda (_x) "foo") '(1 nil 3)) => '("foo" "foo" "foo") ; native + lambda (mapcar (##prog1 "foo" %) '(1 nil 3)) => '("foo" "foo" "foo") ; native + llama (mapcar (##prog2 % "foo") '(1 nil 3)) => '("foo" "foo" "foo") ; native + llama (-map (-const "foo") '(1 nil 3)) => '("foo" "foo" "foo") ; dash + compiled-fun (--map "foo" '(1 nil 3)) => '("foo" "foo" "foo") ; dash + form (anaphoric)
A number:
(funcall (lambda (&rest _) 42) 1 2 "foo" 3) => 42 ; lambda (funcall (##prog1 42 &*) 1 2 "foo" 3) => 42 ; llama (funcall (##+ 42 _&*) 1 2 "foo" 3) => 42 ; llama (funcall (-const 42) 1 2 "foo" 3) => 42 ; dash's compiled-fun
(mapcar (lambda (_x) 42) '(1 nil 3)) => '(42 42 42) ; native + lambda (mapcar (##prog1 42 %) '(1 nil 3)) => '(42 42 42) ; native + llama (mapcar (##prog2 % 42) '(1 nil 3)) => '(42 42 42) ; native + llama (mapcar (##+ 42 _%) '(1 nil 3)) => '(42 42 42) ; native + llama (-map (-const 42) '(1 nil 3)) => '(42 42 42) ; dash + compiled-fun (--map 42 '(1 nil 3)) => '(42 42 42) ; dash + form (anaphoric)
When the function is mapcar
, the anaphoric --map
can be used, and this will be by far the shortest solution.
Otherwise -const
will be the shortest one.
-flip
Though restricted to a few cases, -flip
can get rid of lambdas:
(funcall (lambda (x y) (- y x)) 3 7) => 4 ; lambda (funcall (##- %2 %1) 3 7) => 4 ; llama (funcall (-flip #'-) 3 7) => 4 ; dash's compiled-fun
The lambda above could also be rewritten like this:
(funcall (lambda (x y) (- (- x y))) 3 7) => 4 ; lambda (funcall (-compose #'- #'-) 3 7) => 4 ; dash's compiled-fun (funcall (##- (- %1 %2)) 3 7) => 4 ; llama
so that we could use -compose
instead — though that would be longer, as would the llama.
-not
When not
shows up in the beginning of a lambda, that's a good candidate for replacing the lambda with dash's -not
:
(funcall (lambda (x) (not (keywordp x))) 42) => t ; lambda (funcall (##not (keywordp %)) 42) => t ; llama (funcall (-not #'keywordp) 42) => t ; dash's compiled-fun
-orfn
When or
shows up in the beginning of a lambda, that's a good candidate for replacing the lambda with dash's -orfn
:
(seq-filter (lambda (x) (or (stringp x) (keywordp x))) '(:a 1 'b "42" :c)) ; seq + lambda (seq-filter (##or (stringp %) (keywordp %)) '(:a 1 'b "42" :c)) ; seq + llama (--filter (or (stringp it) (keywordp it)) '(:a 1 'b "42" :c)) ; dash + form (anaphoric) (-filter (-orfn #'stringp #'keywordp) '(:a 1 'b "42" :c)) ; dash + compiled-fun => '(:a "42" :c)
-andfn
When and
shows up in the beginning of a lambda, that's a good candidate for replacing the lambda with dash's -andfn
:
With mapcar
:
(mapcar (lambda (x) (and (symbolp x) (symbol-name x))) '(foo 3 4 bar)) ; native + lambda (mapcar (##and (symbolp %) (symbol-name %)) '(foo 3 4 bar)) ; native + llama (--map (and (symbolp it) (symbol-name it)) '(foo 3 4 bar)) ; dash + form (anaphoric) (-map (-andfn #'symbolp #'symbol-name) '(foo 3 4 bar)) ; dash + compiled-fun => '("foo" nil nil "bar")
With seq-filter
:
(seq-filter (lambda (x) (and (numberp x) (> x 1))) '(4 -1 p 2 z)) => '(4 2) ; seq + lambda (-filter (-andfn #'numberp (-cut > <> 1)) '(4 -1 p 2 z)) => '(4 2) ; dash + compiled-fun (seq-filter (##and (numberp %) (> % 1)) '(4 -1 p 2 z)) => '(4 2) ; seq + llama (--filter (and (numberp it) (> it 1)) '(4 -1 p 2 z)) => '(4 2) ; dash + form (anaphoric)
Let's have a look at the types here.
;;;; Types (type-of (-andfn #'numberp (-cut > <> 1))) => 'compiled-function (type-of #'numberp) => 'symbol ;; All of these are lambdas: (type-of (lambda (x) (> x 1))) => 'cons (type-of (-cut > <> 1)) => 'cons (type-of (##> % 1)) => 'cons
-cut
The goal of Dash's -cut
is close to llama's.
Have a look at this example:
(seq-filter (lambda (x) (> x 1)) '(4 0 -1 2 1)) => '(4 2) ; seq + lambda (-filter (-cut > <> 1) '(4 0 -1 2 1)) => '(4 2) ; dash + -cut (seq-filter (##> % 1) '(4 0 -1 2 1)) => '(4 2) ; seq + llama
And -cut
has no need for a funcall
here:
(mapcar (lambda (x) (funcall x 13 14 15)) '(* +)) => '(2730 42) ; lambda (mapcar (##funcall % 13 14 15) '(* +)) => '(2730 42) ; llama (--map (funcall it 13 14 15) '(* +)) => '(2730 42) ; dash (anaphoric) (-map (-cut <> 13 14 15) '(* +)) => '(2730 42) ; dash + -cut
The solution offered by -cut
is generic enough, so let's give it a better look.
When it comes to length:
- "-cut " is longer than "##"
- "<>" is longer than "%" (but same as "%2", "&3", etc)
-cut
is shorter when%1
is the very function to be replaced, for which llama would need afuncall
(funcall (lambda (x y z) (vector t t t x t y z)) nil nil t) => [t t t nil t nil t] ; lambda (funcall (-cut vector t t t <> t <> <>) nil nil t) => [t t t nil t nil t] ; -cut (funcall (##vector t t t %1 t %2 %3) nil nil t) => [t t t nil t nil t] ; llama
(mapcar (lambda (x) (funcall x -4)) `(abs ,(lambda (x) (+ x 6)))) => '(4 2) ; native + 2 lambdas (mapcar (##funcall % -4) `(abs ,(##+ % 6))) => '(4 2) ; native + 2 llamas (-map (-cut <> -4) `(abs ,(-cut + <> 6))) => '(4 2) ; dash + 2 -cuts
So it's not much of a stretch to say that dash already has a llama-like solution for "simple lambdas with well-ordered arguments".
Unfortunately, -cut
has limitations in flexibility:
-cut apparently cannot | as in |
---|---|
(directly) put args in sublists | (##1+ (car %)) |
deal with args in different order | (##- %3 %2 %5) |
repeat args | (##+ % %) |
Let's look at these "repeat arguments" case:
(mapconcat (lambda (a) (concat a a)) '("x" "y") " ") => "xx yy" (mapconcat (##concat % %) '("x" "y") " ") => "xx yy"
Here it seems it has to be a llama: dash will simply... not -cut
it.
Another limitation is that the <>
of -cut
expects arguments in their direct order. We can't change the order.
Still another limitation is that arguments must be at the same level of the function being called, so sublists don't work. But we could get around this one with some creativity. If we look at that cl-sort
again, we can now see at least six options (of which llama happens to be the shortest):
;; All of the below evaluate to: => '(("1" . "2") ("3" . "9") ("5" . "0"))
;;; regular lambda (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (lambda (pair) (string-to-number (car pair))))
;;; pcase-lambda (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (pcase-lambda (`(,a . ,d)) (string-to-number a)))
;;; llama (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (##string-to-number (car %)))
;;; non-anaphoric dash
;;;; -lambda (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (-lambda ((a . d)) (string-to-number a)))
;;;; -compose (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (-compose #'string-to-number #'car))
;;;; -cut (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (-cut -> <> car string-to-number)) ;; we thread ^ to bring the arg up for -cut
However:
;;;; -cut (doesn't work!) (cl-sort '(("5" . "0") ("1" . "2") ("3" . "9")) #'< :key (-cut string-to-number (car <>))) ;; because here it's in a sublist ^ !!> wrong-number-of-arguments
But dash can't always save you from long lambdas
Sometimes dash won't have an anaphoric, and -cut
won't cut it.
mapatoms
Say we want to know how many primitives our Emacs has.
;;; lambda (let (prims) (mapatoms (lambda (symbol) (and (functionp symbol) (subr-primitive-p (symbol-function symbol)) (push symbol prims)))) (length prims))
;;; llama (let (prims) (mapatoms (##and (functionp %) (subr-primitive-p (symbol-function %)) (push % prims))) (length prims))
There's no dash anaphoric to deal with mapatoms
, and -cut
won't work here.
mapconcat
Same problem.
Here llama looks better, because dash's solution is too long a workaround.
;;; lambda (mapconcat (lambda (x) (format "%s (%s)" x (length x))) '("hi" "world") ", ") => "hi (2), world (5)"
;;; llama (mapconcat (##format "%s (%s)" % (length %)) '("hi" "world") ", ") => "hi (2), world (5)"
;;; dash (--tree-mapreduce (format "%s (%s)" it (length it)) (concat it ", " acc) '("hi" "world")) => "hi (2), world (5)"
Curiously, dash doesn't have a mapconcat
equivalent, neither regular nor anaphoric. If it did, the anaphoric would look like this:
;;;; dash doesn't have this one (yet?) (--mapconcat (format "%s (%s)" it (length it)) ", " '("hi" "world"))
It would replace the lambda with a form and switch the order, making the list suitable for threading last (with ->>
).
And dash might not be available
Finally, maybe you just won't use dash.
Maybe you don't like dash. Or maybe you intend to upstream your code to Emacs, and are avoiding dash as a dependency because, alas, it isn't native. In those cases, dash's functions would of course not be an option. Nor would xht's, since it requires dash.
Should you then choose llama?
The attractiveness of llama depends on a few factors
Let's compare it with its alternatives.
llama vs. anaphorics
Whether llama is a good alternative to anaphorics will depend on factors such as:
- Whether a library with anaphorics (notably dash) is already being required
- Whether a library with anaphorics (notably dash) could be required (if not yet) with little cost
- Whether an exact anaphoric option would exist for the lambda to be replaced
- Whether llama itself could be required with little cost
- The arity of the lambda to be replaced
- Personal preference
llama vs. function combinators
Dash's function combinators can sometimes solve your problem better than an anaphoric would.
Whether to use them instead of llama may depend on:
- Whether you're already requiring dash
- Whether you're already requiring llama
- Whether the cost of additionally requiring one of them would be low
- Whether you're comfortable with dash's function combinators
- Questions of size and readability, which will vary from case to case
- Questions of flexibility, because combinators can't always solve the lambda you want
It's a bit of a mixed bag.
llama vs. regular long lambdas
When dash is not an option, should you use llama to shorten your lambdas?
How compelling this option is might depend on the lambdas you're faced with.
complex lambdas
It's an inherent limitation of llama that it obscures the nature of the input arguments that it defines. This is by design, a seemingly unavoidable side-effect of it being able to create and pass arguments (%
, %2
, &3
, &*
…) on the fly.
In our opening example we had this:
(lambda (foo bar &optional quux &rest more-stuff) (and foo bar (append quux more-stuff))) ; regular lambda (##and % %2 (append &3 &*)) ; llama
But looking just at the llama gives you little idea of what that &3
stands for.
Although llama makes it much more compact, the long lambda gives you a much better hint of what it is you'll be passing to it (we need a foo
and a bar
; a quux
may be useful as well; and more-stuff
, if that's available).
So what llama gives you here in convenience and shortness, it takes away in readability: the longer the lambda, the less clear the nature of the input it expects to receive.
How to deal with this will depend on the situation.
- If you needed to call one such lambda repeatedly, you could for example use llamas, and add a comment near them about the variables involved.
- If however you need to call it once, and the context doesn't help, you might as well go with the regular lambda.
The point here is that even if you could use llama to replace a lambda like this one, you may prefer not to because it would obscure the nature of the arguments.
simple lambdas
At the other end of the spectrum, we have very simple monadic lambdas that are passed to functions whose nature of the input is clear to you, or at least easy to infer. In those cases, llama has much improved readability.
That's good news. Of the lambdas required by functions, most are monadic. And mapcar
and mapc
are ubiquitous.
This means that the greatest part of the explicit long lambdas that you're likely to meet while coding can be shortened by llamas with little to no cost in readability. They're things such as this:
;; from simple.el (defun minibuffer-default-add-shell-commands () ... (setq commands (mapcar (lambda (command) (concat command " " filename)) commands)) ...)
which llama would turn into just this:
(defun minibuffer-default-add-shell-commands () ... (setq commands (mapcar (##concat % " " filename) commands)) ...)
You, being used to mapcar
, and faced with the variable commands
, which you know must be a list, will then quickly see that the %
refers to a command.
Therefore, if you are a dash-averse person who would nevertheless like to see your long lambdas gone, then llama should be an attractive option to you.
See next
Here we looked at the case where you are still unsure whether to pick llama or pick a library full of anaphorics.
Now suppose you have already decided that you'll be using one such library. Should you then use its anaphorics? Or should you use their non-anaphoric counterparts plus a llama?
We give this question a closer look in Part II.