`thunk-let'?

classic Classic list List threaded Threaded
77 messages Options
1234
Reply | Threaded
Open this post in threaded view
|

`thunk-let'?

Michael Heerdegen
Hi,

I often want thunks (like in "thunk.el") when I'm not sure if it's
necessary to do some calculation (e.g. the result is used in an `if'
branch) and want to save the time if not.  So I bind a variable not to
the expression to eval but to a thunk evaluating it, and use
`thunk-force' everywhere I need to refer to the value.

It would be cool if the programmer wouldn't need to speak that out, if
you could hide away the details and use normal variables instead.  You
would have a kind of `let' that would look like making bindings to
variables just like `let' but the expressions that are assigned are not
evaluated before they are needed - maybe never, but also maximally once.

I then realized that doing this should be trivial thanks to
`cl-symbol-macrolet': instead of binding the original variables, you
bind the expressions - wrapped inside `thunk-delay' - to helper vars.
Then, you `symbol-macrolet' all original variables to a `thunk-force' of
the according helper variable:

#+begin_src emacs-lisp
;; -*- lexical-binding: t -*-

(eval-when-compile
  (require 'cl-lib))

(defmacro thunk-let (bindings &rest body)
  "Like `let' but make delayed bindings.
This is like `let' but all binding expressions are not calculated
before they are used."
  (declare (indent 1))
  (setq bindings
        (mapcar (pcase-lambda (`(,var ,binding))
                  (list (make-symbol (concat (symbol-name var) "_thunk"))
                        var binding))
                bindings))
  `(let ,(mapcar
          (pcase-lambda (`(,thunk-var ,_var ,binding))
            `(,thunk-var (thunk-delay ,binding)))
          bindings)
     (cl-symbol-macrolet
         ,(mapcar (pcase-lambda (`(,thunk-var ,var ,_binding))
                    `(,var (thunk-force ,thunk-var)))
                  bindings)
       ,@body)))

(defmacro thunk-let* (bindings &rest body)
  "Like `let*' but make delayed bindings.
This is like `let*' but all binding expressions are not calculated
before they are used."
  (declare (indent 1))
  (if (> (length bindings) 1)
      `(thunk-let (,(car bindings))
         (thunk-let ,(cdr bindings)
           ,@body))
    `(thunk-let ,bindings ,@body)))
#+end_src

An alternative name would be `delayed-let'.  I think it would be very
convenient.


Here is a playground example to test when and if something is calculated
(you need lexical-binding mode):

#+begin_src emacs-lisp
(defmacro calculate-with-message (varname expression)
  `(progn (message "Calculating %s..." ,varname)
          (sit-for 2)
          (prog1 ,expression
            (message "Calculating %s...done" ,varname)
            (sit-for 1))))

(thunk-let ((a (calculate-with-message "a" (+ 1 2)))
            (b (calculate-with-message "b" (* 10 3))))
  (1+ b))
;; etc.
#+end_src



What do people think about this idea?


Thanks,

Michael.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Michael Heerdegen
Michael Heerdegen <[hidden email]> writes:

> I then realized that doing this should be trivial thanks to
> `cl-symbol-macrolet': instead of binding the original variables, you
> bind the expressions - wrapped inside `thunk-delay' - to helper vars.
> Then, you `symbol-macrolet' all original variables to a `thunk-force' of
> the according helper variable:

One limitation of that way of implementation would of course be that you
can't set the bound variables in the BODY, e.g. this would not work:

#+begin_src emacs-lisp
(thunk-let ((a (+ 1 2)))
  (setq a (1+ a))
  a)
#+end_src


Michael.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Stefan Monnier
In reply to this post by Michael Heerdegen
> An alternative name would be `delayed-let'.  I think it would be very
> convenient.

Another name could be `lazy-let`.  But if you add it to thunk.el, then
thunk-let sounds like the better name.


        Stefan

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Nicolas Petton-2
In reply to this post by Michael Heerdegen
Michael Heerdegen <[hidden email]> writes:

> Hi,

Hi Michael,

> An alternative name would be `delayed-let'.  I think it would be very
> convenient.

I think `thunk-let' is a good name.

> What do people think about this idea?

I like it!

Cheers,
Nico

signature.asc (497 bytes) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Michael Heerdegen
In reply to this post by Stefan Monnier
Stefan Monnier <[hidden email]> writes:

> Another name could be `lazy-let`.  But if you add it to thunk.el, then
> thunk-let sounds like the better name.

Ok, so let's be brave and aim to add it as `lazy-let' to subr-x.

There is a question I want us to think about: what should the semantics
of a `lazy-let' bound variable be if it is bound or set inside the BODY?
I guess we can't inhibit that further references to the variable are
also translated into calls of `thunk-force'.  Which would mean
we would have to make `thunk-force' setf'able (or introduce a new
atrifical place form for that purpose).

If I do this, the second question is whether any rebinds or sets of
these variables should implicitly create lazy values again or not.  In
the first case, I would have to use a modified version of
`cl-symbol-macrolet' to make that work, but I think it should be
doable.  The second case should be trivial, that's what we get with
`cl-symbol-macrolet' out of the box.

I wonder which behavior people would prefer.


Michael.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Michael Heerdegen
Michael Heerdegen <[hidden email]> writes:

>  The second case should be trivial, that's what we get with
> `cl-symbol-macrolet' out of the box.

I tried the first way: I implemented a place `lazy-let--thunk-value'
that is only used internally, and went with `cl-symbol-macrolet'.

Now doing the e.g. the following works (lexical-binding needed!):

#+begin_src emacs-lisp
(lazy-let ((a (+ 1 2))
           (x (error "This evaluation would raise an error!"))
           (b (* 10 3))
           (c (* 10 10 10)))
  (setq a (+ b a)) ;  33
  (cl-incf b)      ;  31
  (+ a b c))

==> 1064
#+end_src

Here is the implementation:

#+begin_src emacs-lisp
;; -*- lexical-binding: t -*-

(eval-when-compile
  (require 'cl-lib))

(defun lazy-let--thunk-value (thunk) (funcall thunk))
(defun lazy-let--set-thunk-value (thunk value) (funcall thunk value))
(gv-define-simple-setter lazy-let--thunk-value lazy-let--set-thunk-value)

(defmacro lazy-let--make-thunk (expr)
  (let ((forced (make-symbol "forced"))
        (val    (make-symbol "val"))
        (args   (make-symbol "args")))
    `(let (,forced ,val)
       (lambda (&rest ,args)
         (if ,args
             (prog1 (setf ,val (car ,args))
               (setq ,forced t))
           (unless ,forced
             (setf ,val ,expr)
             (setf ,forced t))
           ,val)))))

(defmacro lazy-let (bindings &rest body)
  "Like `let' but make delayed bindings.
This is like `let' but all binding expressions are not calculated
before they are used."
  (declare (indent 1))
  (setq bindings
        (mapcar (pcase-lambda (`(,var ,binding))
                  (list (make-symbol (concat (symbol-name var) "_thunk"))
                        var binding))
                bindings))
  `(let ,(mapcar
          (pcase-lambda (`(,thunk-var ,_var ,binding))
            `(,thunk-var (lazy-let--make-thunk ,binding)))
          bindings)
     (cl-symbol-macrolet
         ,(mapcar (pcase-lambda (`(,thunk-var ,var ,_binding))
                    `(,var (lazy-let--thunk-value ,thunk-var)))
                  bindings)
       ,@body)))

(defmacro lazy-let* (bindings &rest body)
  "Like `let*' but make delayed bindings.
This is like `let*' but all binding expressions are not calculated
before they are used."
  (declare (indent 1))
  (if (> (length bindings) 1)
      `(lazy-let (,(car bindings))
         (lazy-let ,(cdr bindings)
           ,@body))
    `(lazy-let ,bindings ,@body)))


#+end_src


Regards,

Michael.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Michael Heerdegen
Michael Heerdegen <[hidden email]> writes:

> I tried the first way: I implemented a place `lazy-let--thunk-value'
> that is only used internally, and went with `cl-symbol-macrolet'.

BTW, with this kind of implementation,

#+begin_src emacs-lisp
(lazy-let* ((a 3)
            (b (- a 3)))
  (setq a 5)
  b)

==> 2
#+end_src

I guess whether that result is surprising depends on your mental model.


Michael.

Reply | Threaded
Open this post in threaded view
|

[SUSPECTED SPAM] Re: `thunk-let'?

Stefan Monnier
In reply to this post by Michael Heerdegen
>> Another name could be `lazy-let`.  But if you add it to thunk.el, then
>> thunk-let sounds like the better name.
> Ok, so let's be brave and aim to add it as `lazy-let' to subr-x.
> There is a question I want us to think about: what should the semantics
> of a `lazy-let' bound variable be if it is bound or set inside the BODY?

The semantics of `setq`ing such a var should be: compile-time error.
The semantics of let-rebinding such a variable should be for the new
binding to hide the outer (lazy) one.

cl-symbol-macrolet currently doesn't provide this let-rebinding
semantics, but it should (we already need to fix it for generator.el),
so it's perfectly fine for the new code to just use cl-symbol-macrolet
and then say that if rebinding isn't working right it's due to a (known)
bug in cl-symbol-macrolet.


        Stefan

Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Michael Heerdegen
Stefan Monnier <[hidden email]> writes:

> > Ok, so let's be brave and aim to add it as `lazy-let' to subr-x.
> > There is a question I want us to think about: what should the semantics
> > of a `lazy-let' bound variable be if it is bound or set inside the
> > BODY?
>
> The semantics of `setq`ing such a var should be: compile-time error.
> The semantics of let-rebinding such a variable should be for the new
> binding to hide the outer (lazy) one.

Is this patch acceptable (also attached as file)?


From 4941becd07f6ffbe387006248193d95b258be526 Mon Sep 17 00:00:00 2001
From: Michael Heerdegen <[hidden email]>
Date: Thu, 2 Nov 2017 18:45:34 +0100
Subject: [PATCH] Add macros `lazy-let' and `lazy-let*'

* lisp/emacs-lisp/subr-x.el (lazy-let, lazy-let*): New macros.
* lisp/emacs-lisp/thunk.el (thunk-delay, thunk-force): Add autoload
cookies.
* test/lisp/emacs-lisp/subr-x-tests.el: Use lexical-binding.
(subr-x-lazy-let-basic-test, subr-x-lazy-let*-basic-test)
(subr-x-lazy-let-bound-vars-cant-be-bound-test)
(subr-x-lazy-let-lazyness-test, subr-x-lazy-let*-lazyness-test)
(subr-x-lazy-let-bad-binding-test): New tests for `lazy-let' and `lazy-let*.
---
 etc/NEWS                             |  4 +++
 lisp/emacs-lisp/subr-x.el            | 48 ++++++++++++++++++++++++++++++++++
 lisp/emacs-lisp/thunk.el             |  2 ++
 test/lisp/emacs-lisp/subr-x-tests.el | 50 +++++++++++++++++++++++++++++++++++-
 4 files changed, 103 insertions(+), 1 deletion(-)

diff --git a/etc/NEWS b/etc/NEWS
index c47ca42d27..8b1f659ebf 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -128,6 +128,10 @@ calling 'eldoc-message' directly.
 
 * Lisp Changes in Emacs 27.1
 
+---
+** The new macros 'lazy-let' and 'lazy-let*' are analogue to `let' and
+`let*' but create bindings that are evaluated lazily.
+
 ---
 ** The 'file-system-info' function is now available on all platforms.
 instead of just Microsoft platforms.  This fixes a 'get-free-disk-space'
diff --git a/lisp/emacs-lisp/subr-x.el b/lisp/emacs-lisp/subr-x.el
index 8ed29d8659..ce8956c96f 100644
--- a/lisp/emacs-lisp/subr-x.el
+++ b/lisp/emacs-lisp/subr-x.el
@@ -245,6 +245,54 @@ string-remove-suffix
       (substring string 0 (- (length string) (length suffix)))
     string))
 
+(defmacro lazy-let (bindings &rest body)
+  "Like `let' but make delayed bindings.
+
+This is like `let' but any binding expression is not evaluated
+before the variable is used for the first time.
+
+It is not allowed to set `lazy-let' or `lazy-let*' bound
+variables."
+  (declare (indent 1) (debug let))
+  (cl-callf2 mapcar
+      (lambda (binding)
+        (pcase binding
+          ((or (and (pred symbolp) s)
+               `(,(and (pred symbolp) s)))
+           `(,s nil))
+          (`(,(pred symbolp) ,_) binding)
+          (_ (signal 'error (cons "Bad binding in lazy-let"
+                                  (list binding))))))
+      bindings)
+  (cl-callf2 mapcar
+      (pcase-lambda (`(,var ,binding))
+        (list (make-symbol (concat (symbol-name var) "-thunk"))
+              var binding))
+      bindings)
+  `(let ,(mapcar
+          (pcase-lambda (`(,thunk-var ,_var ,binding))
+            `(,thunk-var (thunk-delay ,binding)))
+          bindings)
+     (cl-symbol-macrolet
+         ,(mapcar (pcase-lambda (`(,thunk-var ,var ,_binding))
+                    `(,var (thunk-force ,thunk-var)))
+                  bindings)
+       ,@body)))
+
+(defmacro lazy-let* (bindings &rest body)
+  "Like `let*' but make delayed bindings.
+
+This is like `let*' but any binding expression is not evaluated
+before the variable is used for the first time.
+
+It is not allowed to set `lazy-let' or `lazy-let*' bound
+variables."
+  (declare (indent 1) (debug let))
+  (cl-reduce
+   (lambda (expr binding) `(lazy-let (,binding) ,expr))
+   (nreverse bindings)
+   :initial-value `(progn ,@body)))
+
 (provide 'subr-x)
 
 ;;; subr-x.el ends here
diff --git a/lisp/emacs-lisp/thunk.el b/lisp/emacs-lisp/thunk.el
index 371d10444b..0c5d0b709e 100644
--- a/lisp/emacs-lisp/thunk.el
+++ b/lisp/emacs-lisp/thunk.el
@@ -46,6 +46,7 @@
 
 (eval-when-compile (require 'cl-macs))
 
+;;;###autoload
 (defmacro thunk-delay (&rest body)
   "Delay the evaluation of BODY."
   (declare (debug t))
@@ -61,6 +62,7 @@ thunk-delay
              (setf ,forced t))
            ,val)))))
 
+;;;###autoload
 (defun thunk-force (delayed)
   "Force the evaluation of DELAYED.
 The result is cached and will be returned on subsequent calls
diff --git a/test/lisp/emacs-lisp/subr-x-tests.el b/test/lisp/emacs-lisp/subr-x-tests.el
index 0e8871d9a9..c477a63a29 100644
--- a/test/lisp/emacs-lisp/subr-x-tests.el
+++ b/test/lisp/emacs-lisp/subr-x-tests.el
@@ -1,4 +1,4 @@
-;;; subr-x-tests.el --- Testing the extended lisp routines
+;;; subr-x-tests.el --- Testing the extended lisp routines    -*- lexical-binding: t -*-
 
 ;; Copyright (C) 2014-2017 Free Software Foundation, Inc.
 
@@ -538,6 +538,54 @@
                    (format "abs sum is: %s"))
                  "abs sum is: 15")))
 
+
+;; lazy-let tests
+
+(ert-deftest subr-x-lazy-let-basic-test ()
+  "Test whether bindings are established."
+  (should (equal (lazy-let ((x 1) (y 2)) (+ x y)) 3)))
+
+(ert-deftest subr-x-lazy-let*-basic-test ()
+  "Test whether bindings are established."
+  (should (equal (lazy-let* ((x 1) (y (+ 1 x))) (+ x y)) 3)))
+
+(ert-deftest subr-x-lazy-let-bound-vars-cant-be-bound-test ()
+  "Test whether setting or binding a `lazy-let' bound variable fails."
+  (should-error (eval '(lazy-let ((x 1)) (let ((y 7)) (setq x (+ x y)) (* 10 x))) t))
+  (should-error (eval '(lazy-let ((x 1)) (let ((x 2)) x)) t)))
+
+(ert-deftest subr-x-lazy-let-lazyness-test ()
+  "Test for lazyness."
+  (should
+   (equal (let ((x-evalled nil)
+                (y-evalled nil))
+            (lazy-let ((x (progn (setq x-evalled t) (+ 1 2)))
+                       (y (progn (setq y-evalled t) (+ 3 4))))
+              (let ((evalled-y y))
+                (list x-evalled y-evalled evalled-y))))
+          (list nil t 7))))
+
+(ert-deftest subr-x-lazy-let*-lazyness-test ()
+  "Test lazyness of `lazy-let*'."
+  (should
+   (equal (let ((x-evalled nil)
+                (y-evalled nil)
+                (z-evalled nil)
+                (a-evalled nil))
+            (lazy-let* ((x (progn (setq x-evalled t) (+ 1 1)))
+                        (y (progn (setq y-evalled t) (+ x 1)))
+                        (z (progn (setq z-evalled t) (+ y 1)))
+                        (a (progn (setq a-evalled t) (+ z 1))))
+              (let ((evalled-z z))
+                (list x-evalled y-evalled z-evalled a-evalled evalled-z))))
+          (list t t t nil 4))))
+
+(ert-deftest subr-x-lazy-let-bad-binding-test ()
+  "Test whether a bad binding causes a compiler error."
+  (should-error (byte-compile (lazy-let ((x 1 1)) x)))
+  (should-error (byte-compile (lazy-let (27) x)))
+  (should-error (byte-compile (lazy-let x x))))
+
 
 (provide 'subr-x-tests)
 ;;; subr-x-tests.el ends here
--
2.14.2




Thanks,

Michael.

0001-Add-macros-lazy-let-and-lazy-let.patch (6K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Stefan Monnier
> +  (cl-callf2 mapcar
> +      (lambda (binding)
> +        (pcase binding
> +          ((or (and (pred symbolp) s)
> +               `(,(and (pred symbolp) s)))
> +           `(,s nil))
> +          (`(,(pred symbolp) ,_) binding)
> +          (_ (signal 'error (cons "Bad binding in lazy-let"
> +                                  (list binding))))))
> +      bindings)

I think lazily binding a variable to nil is useless, so I'd drop the
first pcase branch above.

> +   :initial-value `(progn ,@body)))

You can use `macroexp-progn` to avoid constructing a `progn` in the
common case where body is already a single expression.

> +;;;###autoload
>  (defmacro thunk-delay (&rest body)

Maybe a better option is to add a `require` in the expansion of lazy-let?


        Stefan

Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Eli Zaretskii
In reply to this post by Michael Heerdegen
> From: Michael Heerdegen <[hidden email]>
> Date: Wed, 08 Nov 2017 18:22:51 +0100
> Cc: Nicolas Petton <[hidden email]>, Emacs Development <[hidden email]>

Thanks.  I have a few comments to the documentation parts:

> +---
> +** The new macros 'lazy-let' and 'lazy-let*' are analogue to `let' and
> +`let*' but create bindings that are evaluated lazily.

Given that "lazy evaluation" seems not to be described anywhere in the
ELisp manual, I think we cannot get away with "---" here, and will
have to add at least something to the manual.

> +(defmacro lazy-let (bindings &rest body)
> +  "Like `let' but make delayed bindings.

And this seems to use a different term for this.

> +This is like `let' but any binding expression is not evaluated
> +before the variable is used for the first time.

The "like let" part is a repetition of what was already said in the
first sentence, so there's no need to repeat it.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Michael Heerdegen
Eli Zaretskii <[hidden email]> writes:

> Given that "lazy evaluation" seems not to be described anywhere in the
> ELisp manual, I think we cannot get away with "---" here, and will
> have to add at least something to the manual.

Well, it's in subr-x because I'm not sure that it is yet a good idea to
"advertize it so loudly" as Stefan uses to say.  Nothing in subr-x is
described in the manual.

If we are sure that we want to document this right now in the manual, we
could document it together with thunks.  Though, I'm also not sure if
thunks won't be replaced with something more advanced in the near
future.


> > +(defmacro lazy-let (bindings &rest body)
> > +  "Like `let' but make delayed bindings.
>
> And this seems to use a different term for this.

Yes, I'll make this more consistent, thanks.


Michael.

Reply | Threaded
Open this post in threaded view
|

RE: `thunk-let'?

Drew Adams
> Well, it's in subr-x because I'm not sure that it is yet a good idea to
> "advertize it so loudly" as Stefan uses to say.  Nothing in subr-x is
> described in the manual.

This is not a good approach, IMHO.  Whether we should have
a library that is for "half-baked" stuff is debatable.

But even if it is decided to do that (why?), I don't think
it makes sense to decide whether something gets documented
in the manuals based on how well baked we think it is.
That's a bad criterion to use (IMHO).

And it invites things to fall through the cracks and not
be documented even after they get improved.

The usual criteria for deciding whether something gets
documented in a manual should apply to half-baked
features also.

We should WANT users to try half-baked stuff that we
deliver.  That's how it gets improved.  Not advertizing
something just because it is still iffy is not advisable.

An exception could exist for something that we think
might prove dangerous in some way (e.g. possible data
loss).  But probably something like that should not be
delivered at all - it should just continue to be tested
and improved "internally" until it is not problematic.

If something is TOO iffy then just don't deliver it.
Don't use _doc_ as way of "hiding" something that is
unsure.

My "vote" is to move the stuff in subr.el to where it
belongs, thing by thing, place by place, and to
document any of it that we would normally document,
with no concern about how well baked we might think it is.

> If we are sure that we want to document this right now
> in the manual...

There should be no "right now" based on how finished
something is.  If we really feel that something is
not ready for release then it should just be pulled
from the release.  If something is distributed then
it should be supported normally.

Just one opinion.

Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Michael Heerdegen
In reply to this post by Eli Zaretskii
Eli Zaretskii <[hidden email]> writes:

> Given that "lazy evaluation" seems not to be described anywhere in the
> ELisp manual, I think we cannot get away with "---" here, and will
> have to add at least something to the manual.

Ok, here is a first draft of how I could imagine an addition to the
manual (as plain txt):


Deferred and lazy evaluation

Sometimes it is useful to delay the evaluation of an expression, for
example if you want to avoid to perform a time-consuming calculation
in the case that it turns out that the result is not needed in the
future of the program.


-- Macro: thunk-delay FORMS...

Return a thunk for evaluating the FORMS.  A thunk is a closure that
evaluates the FORMS in the lexical environment present when
`thunk-delay' has been called.


-- Function: thunk-force THUNK

Force a thunk to perform the evaluation of the FORMS specified to the
`thunk-delay' that created the thunk.  The result of the evaluation of
the last form is returned.  The THUNK also "remembers" that it has
been forced: Any further calls of `thunk-force' on the same THUNK will
just return the same result without evaluating the FORMS again.


-- Macro: lazy-let (bindings...) forms...

This macro is analogue to `let' but creates "lazy" variable bindings.
Any binding has the form (SYMBOL VALUE-FORM).  Unlike `let', the
evaluation of any VALUE-FORM is deferred until the binding of the
according SYMBOL is used for the first time when evaluating the FORMS.
Any VALUE-FORM is evaluated maximally once.

Example:

(defun f (number)
  (lazy-let ((derived-number
              (progn (message "Calculating 1 plus 2 times %d" number)
                     (1+ (* 2 number)))))
    (if (> number 10)
        derived-number
      number)))

(f 5)
==> 5

(f 12)
|--> "Calculating 1 plus 2 times 12"
25

Because of the special nature of lazily bound variables, it is an
error to set them (e.g. with `setq').


-- Macro: lazy-let* (bindings...) forms...

This is like `lazy-let' but any expression in BINDINGS is allowed to
refer to preceding bindings in this `lazy-let*' form.


Example:

(lazy-let* ((x (prog2 (message "Calculating x...")
                   (+ 1 1)
                 (message "Finished calculating x")))
            (y (prog2 (message "Calculating y...")
                   (+ x 1)
                 (message "Finished calculating y")))
            (z (prog2 (message "Calculating z...")
                   (+ y 1)
                 (message "Finished calculating z")))
            (a (prog2 (message "Calculating a...")
                   (+ z 1)
                 (message "Finished calculating a"))))
  (* z x))

|--> Calculating z...
|--> Calculating y...
|--> Calculating x...
|--> Finished calculating x
|--> Finished calculating y
|--> Finished calculating z

==> 8


`lazy-let' and `lazy-let*' use thunks implicitly: their expansion
creates helper symbols and binds them to thunks wrapping the binding
expressions.  All rerences to the original variables in the FORMS are
then replaced by an expression that calls `thunk-force' on the
according helper variable.  So, any code using `lazy-let' or
`lazy-let*' could be rewritten to use thunks, but in many cases using
these macros results in nicer code than using thunks explicitly.



WDYT?  I would definitely need a bit of help to turn this into texinfo
with good Enlish.


Thanks,

Michael.


Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Michael Heerdegen
In reply to this post by Stefan Monnier
Stefan Monnier <[hidden email]> writes:

> I think lazily binding a variable to nil is useless, so I'd drop the
> first pcase branch above.

Makes sense -- done.


> > +   :initial-value `(progn ,@body)))
>
> You can use `macroexp-progn` to avoid constructing a `progn` in the
> common case where body is already a single expression.

Done (though the byte compiler already dismissed this redundant `progn'
in every case, so I didn't care about it).


> > +;;;###autoload
> >  (defmacro thunk-delay (&rest body)
>
> Maybe a better option is to add a `require` in the expansion of lazy-let?

That should be fine, so I've done it as well.


This is the updated (cumulative) patch:


From f8bb4d2872c366af45032a3d8d1d2e907c3e4d9d Mon Sep 17 00:00:00 2001
From: Michael Heerdegen <[hidden email]>
Date: Thu, 2 Nov 2017 18:45:34 +0100
Subject: [PATCH] Add macros `lazy-let' and `lazy-let*'

* lisp/emacs-lisp/subr-x.el (lazy-let, lazy-let*): New macros.
* test/lisp/emacs-lisp/subr-x-tests.el: Use lexical-binding.
(subr-x-lazy-let-basic-test, subr-x-lazy-let*-basic-test)
(subr-x-lazy-let-bound-vars-cant-be-bound-test)
(subr-x-lazy-let-lazyness-test, subr-x-lazy-let*-lazyness-test)
(subr-x-lazy-let-bad-binding-test): New tests for `lazy-let' and `lazy-let*.
---
 etc/NEWS                             |  3 +++
 lisp/emacs-lisp/subr-x.el            | 49 +++++++++++++++++++++++++++++++++++
 test/lisp/emacs-lisp/subr-x-tests.el | 50 +++++++++++++++++++++++++++++++++++-
 3 files changed, 101 insertions(+), 1 deletion(-)

diff --git a/etc/NEWS b/etc/NEWS
index c47ca42d27..451688c665 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -128,6 +128,9 @@ calling 'eldoc-message' directly.
 
 * Lisp Changes in Emacs 27.1
 
+** The new macros 'lazy-let' and 'lazy-let*' are analogue to `let' and
+`let*' but create bindings that are evaluated lazily.
+
 ---
 ** The 'file-system-info' function is now available on all platforms.
 instead of just Microsoft platforms.  This fixes a 'get-free-disk-space'
diff --git a/lisp/emacs-lisp/subr-x.el b/lisp/emacs-lisp/subr-x.el
index 8ed29d8659..8e64cbd4ce 100644
--- a/lisp/emacs-lisp/subr-x.el
+++ b/lisp/emacs-lisp/subr-x.el
@@ -245,6 +245,55 @@ string-remove-suffix
       (substring string 0 (- (length string) (length suffix)))
     string))
 
+(defmacro lazy-let (bindings &rest body)
+  "Like `let' but create lazy bindings.
+
+BINDINGS is a list of elements of the form (SYMBOL EXPRESSION).
+Any binding EXPRESSION is not evaluated before the variable
+SYMBOL is used for the first time.
+
+It is not allowed to set `lazy-let' or `lazy-let*' bound
+variables."
+  (declare (indent 1) (debug let))
+  (cl-callf2 mapcar
+      (lambda (binding)
+        (pcase binding
+          (`(,(pred symbolp) ,_) binding)
+          (_ (signal 'error (cons "Bad binding in lazy-let"
+                                  (list binding))))))
+      bindings)
+  (cl-callf2 mapcar
+      (pcase-lambda (`(,var ,binding))
+        (list (make-symbol (concat (symbol-name var) "-thunk"))
+              var binding))
+      bindings)
+  `(progn
+     (require 'thunk)
+     (let ,(mapcar
+            (pcase-lambda (`(,thunk-var ,_var ,binding))
+              `(,thunk-var (thunk-delay ,binding)))
+            bindings)
+       (cl-symbol-macrolet
+           ,(mapcar (pcase-lambda (`(,thunk-var ,var ,_binding))
+                      `(,var (thunk-force ,thunk-var)))
+                    bindings)
+         ,@body))))
+
+(defmacro lazy-let* (bindings &rest body)
+  "Like `let*' but create lazy bindings.
+
+BINDINGS is a list of elements of the form (SYMBOL EXPRESSION).
+Any binding EXPRESSION is not evaluated before the variable
+SYMBOL is used for the first time.
+
+It is not allowed to set `lazy-let' or `lazy-let*' bound
+variables."
+  (declare (indent 1) (debug let))
+  (cl-reduce
+   (lambda (expr binding) `(lazy-let (,binding) ,expr))
+   (nreverse bindings)
+   :initial-value (macroexp-progn body)))
+
 (provide 'subr-x)
 
 ;;; subr-x.el ends here
diff --git a/test/lisp/emacs-lisp/subr-x-tests.el b/test/lisp/emacs-lisp/subr-x-tests.el
index 0e8871d9a9..c477a63a29 100644
--- a/test/lisp/emacs-lisp/subr-x-tests.el
+++ b/test/lisp/emacs-lisp/subr-x-tests.el
@@ -1,4 +1,4 @@
-;;; subr-x-tests.el --- Testing the extended lisp routines
+;;; subr-x-tests.el --- Testing the extended lisp routines    -*- lexical-binding: t -*-
 
 ;; Copyright (C) 2014-2017 Free Software Foundation, Inc.
 
@@ -538,6 +538,54 @@
                    (format "abs sum is: %s"))
                  "abs sum is: 15")))
 
+
+;; lazy-let tests
+
+(ert-deftest subr-x-lazy-let-basic-test ()
+  "Test whether bindings are established."
+  (should (equal (lazy-let ((x 1) (y 2)) (+ x y)) 3)))
+
+(ert-deftest subr-x-lazy-let*-basic-test ()
+  "Test whether bindings are established."
+  (should (equal (lazy-let* ((x 1) (y (+ 1 x))) (+ x y)) 3)))
+
+(ert-deftest subr-x-lazy-let-bound-vars-cant-be-bound-test ()
+  "Test whether setting or binding a `lazy-let' bound variable fails."
+  (should-error (eval '(lazy-let ((x 1)) (let ((y 7)) (setq x (+ x y)) (* 10 x))) t))
+  (should-error (eval '(lazy-let ((x 1)) (let ((x 2)) x)) t)))
+
+(ert-deftest subr-x-lazy-let-lazyness-test ()
+  "Test for lazyness."
+  (should
+   (equal (let ((x-evalled nil)
+                (y-evalled nil))
+            (lazy-let ((x (progn (setq x-evalled t) (+ 1 2)))
+                       (y (progn (setq y-evalled t) (+ 3 4))))
+              (let ((evalled-y y))
+                (list x-evalled y-evalled evalled-y))))
+          (list nil t 7))))
+
+(ert-deftest subr-x-lazy-let*-lazyness-test ()
+  "Test lazyness of `lazy-let*'."
+  (should
+   (equal (let ((x-evalled nil)
+                (y-evalled nil)
+                (z-evalled nil)
+                (a-evalled nil))
+            (lazy-let* ((x (progn (setq x-evalled t) (+ 1 1)))
+                        (y (progn (setq y-evalled t) (+ x 1)))
+                        (z (progn (setq z-evalled t) (+ y 1)))
+                        (a (progn (setq a-evalled t) (+ z 1))))
+              (let ((evalled-z z))
+                (list x-evalled y-evalled z-evalled a-evalled evalled-z))))
+          (list t t t nil 4))))
+
+(ert-deftest subr-x-lazy-let-bad-binding-test ()
+  "Test whether a bad binding causes a compiler error."
+  (should-error (byte-compile (lazy-let ((x 1 1)) x)))
+  (should-error (byte-compile (lazy-let (27) x)))
+  (should-error (byte-compile (lazy-let x x))))
+
 
 (provide 'subr-x-tests)
 ;;; subr-x-tests.el ends here
--
2.14.2




Thanks,

Michael.

0001-Add-macros-lazy-let-and-lazy-let.patch (5K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Michael Heerdegen
In reply to this post by Eli Zaretskii
Eli Zaretskii <[hidden email]> writes:

> > +(defmacro lazy-let (bindings &rest body)
> > +  "Like `let' but make delayed bindings.
>
> And this seems to use a different term for this.
>
> > +This is like `let' but any binding expression is not evaluated
> > +before the variable is used for the first time.
>
> The "like let" part is a repetition of what was already said in the
> first sentence, so there's no need to repeat it.

Done.  The updated cumulative patch is in the answer
<[hidden email]> to Stefan's response.

Thanks for reviewing, Eli.


Regards,

Michael.

Reply | Threaded
Open this post in threaded view
|

Re: [SUSPECTED SPAM] Re: `thunk-let'?

Eli Zaretskii
In reply to this post by Michael Heerdegen
> From: Michael Heerdegen <[hidden email]>
> Cc: [hidden email],  [hidden email],  [hidden email]
> Date: Thu, 09 Nov 2017 15:34:30 +0100
>
> Ok, here is a first draft of how I could imagine an addition to the
> manual (as plain txt):

Thanks.

> -- Function: thunk-force THUNK
>
> Force a thunk to perform the evaluation of the FORMS specified to the
> `thunk-delay' that created the thunk.

This should have FORMS in lower-case and THUNK in upper-case

> -- Macro: lazy-let (bindings...) forms...
>
> This macro is analogue to `let' but creates "lazy" variable bindings.
                ^^^^^^^^
"analogous"

> Any binding has the form (SYMBOL VALUE-FORM).  Unlike `let', the
> evaluation of any VALUE-FORM is deferred until the binding of the
> according SYMBOL is used for the first time when evaluating the FORMS.
> Any VALUE-FORM is evaluated maximally once.
                              ^^^^^^^^^
"at most"

> WDYT?

It's good, thank you.

Let me know if you need specific help with Texinfo-izing this.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Eli Zaretskii
In reply to this post by Michael Heerdegen
> From: Michael Heerdegen <[hidden email]>
> Cc: [hidden email],  [hidden email],  [hidden email]
> Date: Wed, 08 Nov 2017 23:22:09 +0100
>
> Eli Zaretskii <[hidden email]> writes:
>
> > Given that "lazy evaluation" seems not to be described anywhere in the
> > ELisp manual, I think we cannot get away with "---" here, and will
> > have to add at least something to the manual.
>
> Well, it's in subr-x because I'm not sure that it is yet a good idea to
> "advertize it so loudly" as Stefan uses to say.  Nothing in subr-x is
> described in the manual.

I'm not sure this should be in subr-x.el.  That file is AFAIU for
stuff used _internally_ in Emacs, which this macro isn't.

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Clément Pit-Claudel
On 2017-11-09 12:20, Eli Zaretskii wrote:

>> From: Michael Heerdegen <[hidden email]>
>> Cc: [hidden email],  [hidden email],  [hidden email]
>> Date: Wed, 08 Nov 2017 23:22:09 +0100
>>
>> Eli Zaretskii <[hidden email]> writes:
>>
>>> Given that "lazy evaluation" seems not to be described anywhere in the
>>> ELisp manual, I think we cannot get away with "---" here, and will
>>> have to add at least something to the manual.
>>
>> Well, it's in subr-x because I'm not sure that it is yet a good idea to
>> "advertize it so loudly" as Stefan uses to say.  Nothing in subr-x is
>> described in the manual.
>
> I'm not sure this should be in subr-x.el.  That file is AFAIU for
> stuff used _internally_ in Emacs, which this macro isn't.

Why doesn't that macro go to ELPA first, possibly to be integrated in Emacs proper if it's found to be useful?

Reply | Threaded
Open this post in threaded view
|

Re: `thunk-let'?

Michael Heerdegen
Clément Pit-Claudel <[hidden email]> writes:

> Why doesn't that macro go to ELPA first, possibly to be integrated in
> Emacs proper if it's found to be useful?

I think it certainly is useful.  And it allows you to write nicer code
because you don't have to use (low-level) thunk objects explicitly
(thus, it's a reasonable abstraction).  I'm just only 97%, and not 100%,
sure that it is the optimal solution for the problem it solves.  So,
"half baked" is an exaggeration.  Anyway, the macro makes it much
easier to write more efficient code in an easy way and clean style, so I
don't doubt it is useful now, in Emacs.


Michael.

1234