reddit / emacs
Lambdas with limited closures
Tohiko · 2022-05-23 08:01
In my functions, I often do the following:
```
(let ((var value) (many-other-vars other-long-value))
(add-hook 'mode-hook (lambda nil (do-something var))))
```
Utilizing lexical binding. This works well, but then I have `many-other-vars` in the closure resulting in unnecessary saving of values and the previously small lambda function becomes quite cumbersome to view in subsequent inspections.
I then resort to the following solution:
```
(defun make-hook (var)
(lambda nil (do-something var)))
(let ((var value) (many-other-vars other-long-value))
(add-hook 'mode-hook (make-hook var)))
```
which then has the desired effect of having only the variable `var` in the closure. But this is counterproductive as now I have to define a function for each lambda I need (defeating the goal of creating anonymous functions on the fly).
Is there a way to limit the closure to specific variables? Other languages have such features (like C++'s lambda functions) but I can't seem to replicate it in emacs-lisp. I tried creating a macro to replicate the solution above but this seems impossible.
deaddyfreddy · 2022-05-23 13:47
> But this is counterproductive as now I have to define a function for each lambda I need
actually no, just put it in an inline lambda
ErnestKz · 2022-05-23 14:35
Hmm yeah interesting, I've also made an attempt with macros but couldn't do it. I tried generating a defun inline, but when the defun is executed it itself captures all the variables in the scope, and passes it onto the lambda it seems.
Tohiko · 2022-05-23 14:51
Yes exactly. Same with writing an inline lambda than returns a lambda. I tried disabling lexical binding for the outer lambda (or defun) and enabling it for the inner one, but that didn't work either.
Tohiko · 2022-05-23 14:52
An inline lambda would capture `many-other-vars` and then pass that to the lambda it creates.
ErnestKz · 2022-05-23 16:24
```
(defmacro mk-lambda (as &rest bs)
`(quote ,`(lambda ,as ,@bs)))
(defun mk-hook (vars lmbda)
`(lambda nil (funcall ,lmbda ,@vars)))
(defmacro mk-ilambda (as &rest bs)
`(mk-hook (list ,@as) (mk-lambda ,as ,@bs)))
(let ((a1 "a")
(a2 "b")
(a3 "c")
(a4 'something-big))
(mk-ilambda (a1 a2 a3)
(print a1)
(print a2)
(print a3)))
```
Something along the lines of this?
Not sure if it'll work in a hook, but `funcall` works on it.
axaxaxasmloe · 2022-05-23 17:20
This is ugly, but works:
(defmacro clambda (vars &rest lambda-body)
`(eval '(let ,(mapcar (lambda (sym) `(,sym ,sym)) vars)
(lambda ,@lambda-body))
t))
`vars` is the list of lexical variables to capture, `lambda-body` is passed to `lambda`:
(funcall (let ((a 1)
(b 2))
(clambda (a) (x) (cons a x)))
3)
=> (1 . 3)
Changing it to `(cons b x)` gives an error, since `b` is not captured.
----
Edit: it unfortunately doesn't handle multiple closures with shared captures, like setters and getters:
(let ((a nil) (b nil))
(cons (clambda (a) (x) (setq a x))
(clambda (a) () a)))
In the corresponding `lambda` version, the closures share `a`.
----
Edit 2: It does work using this modification:
(defmacro capture (vars &rest body)
(declare (indent defun))
`(eval '(let ,(mapcar (lambda (sym) (list sym sym)) vars)
,@body)
t))
The first example now becomes:
(let ((a 1) (b 2))
(capture (a)
(lambda (x) (cons a x))))
which again only captures `a` but not `b`, but now it's possible to return two lambdas from the same `capture` block that share a lexical environment:
(pcase-let
((`(,setter . ,getter)
(let ((a nil)
(b nil))
(capture (a)
(cons (lambda (x) (setq a x))
(lambda () a))))))
(funcall setter 1)
(funcall getter))
arthurno1 · 2022-05-23 21:27
> (let ((var value) (many-other-vars other-long-value)) (add-hook 'mode-hook (lambda nil (do-something var))))
> Is there a way to limit the closure to specific variables?
(let ((var value))
(add-hook 'mode-hook (lambda nil (do-something var)))
(let ((var1 val1) ... (varN valN))
(do-some-computations-with-your-vars)))
That would be probably the simplest way I can think of.
Tohiko · 2022-05-24 09:01
That's brilliant. I didn't know `eval` does not share the same namespace. This is useful to know and the solution is quite great.
I think setters will always be a problem because of something like this:
```
(let ((a 1) (b 2))
(funcall (capture (a)
(lambda (x) (setq a x))) 10)
a)
```
vs something like this:
```
(let ((a 1) (b 2))
(funcall (lambda (x) (setq a x)) 10)
a)
```
axaxaxasmloe · 2022-05-24 09:18
Ah, right, `capture` creates a new environment, so the setter sees a different binding for `a` than the outside code. Yeah, that's probably unsolveable.
I didn't know that about `eval` either, found it by playing around with the problem. I don't fully understand this magic tbh. It's not a completely new namespace, e.g. this works:
(let ((a 1) (b 2)) (capture (a) b))
It appears to only apply to closures.
Phil-Hudson · 2022-05-24 09:28
(let ((var value) (many-other-vars other-long-value))
(add-hook 'mode-hook (defun uniquename nil (do-something var))))
Check it out. It works.
Caveat: the return type of `defun` is undocumented. In (wildly improbable) theory, it *could* change under you.
Tohiko · 2022-05-24 10:05
Tried. It doesn't.
axaxaxasmloe · 2022-05-24 13:08
Another addition: according to [this article](https://nullprogram.com/blog/2013/12/30/) by /u/skeeto, byte compiling the function that generates the closure will get rid of unused captures.
Edit: also [this one](https://nullprogram.com/blog/2017/12/14/)
Phil-Hudson · 2022-05-24 13:28
I use it in many places in my config. Perhaps we have some different value for the term "works".
oantolin · 2022-05-25 03:24
Try byte compiling, I think it does do that optimization.