`thunk-let'?

classic Classic list List threaded Threaded
8 messages Options
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