Commit cc58d4de authored by Michael Heerdegen's avatar Michael Heerdegen

Add macros `thunk-let' and `thunk-let*'

* lisp/emacs-lisp/thunk.el (thunk-let, thunk-let*): New macros.
* test/lisp/emacs-lisp/thunk-tests.el:
(thunk-let-basic-test, thunk-let*-basic-test)
(thunk-let-bound-vars-cant-be-set-test)
(thunk-let-laziness-test, thunk-let*-laziness-test)
(thunk-let-bad-binding-test): New tests for `thunk-let' and
`thunk-let*.

* doc/lispref/eval.texi (Deferred Eval): New section.
* doc/lispref/elisp.texi: Update menu.
parent ef183144
......@@ -455,6 +455,7 @@ Evaluation
the program).
* Backquote:: Easier construction of list structure.
* Eval:: How to invoke the Lisp interpreter explicitly.
* Deferred Eval:: Deferred and lazy evaluation of forms.
Kinds of Forms
......
......@@ -20,11 +20,12 @@ function @code{eval}.
@ifnottex
@menu
* Intro Eval:: Evaluation in the scheme of things.
* Forms:: How various sorts of objects are evaluated.
* Quoting:: Avoiding evaluation (to put constants in the program).
* Backquote:: Easier construction of list structure.
* Eval:: How to invoke the Lisp interpreter explicitly.
* Intro Eval:: Evaluation in the scheme of things.
* Forms:: How various sorts of objects are evaluated.
* Quoting:: Avoiding evaluation (to put constants in the program).
* Backquote:: Easier construction of list structure.
* Eval:: How to invoke the Lisp interpreter explicitly.
* Deferred Eval:: Deferred and lazy evaluation of forms.
@end menu
@node Intro Eval
......@@ -877,3 +878,115 @@ particular elements, like this:
@end group
@end example
@end defvar
@node Deferred Eval
@section Deferred and Lazy Evaluation
@cindex deferred evaluation
@cindex 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. Therefore, the @file{thunk} library provides
the following functions and macros:
@cindex thunk
@defmac thunk-delay forms@dots{}
Return a @dfn{thunk} for evaluating the @var{forms}. A thunk is a
closure (@pxref{Closures}) that inherits the lexical enviroment of the
@code{thunk-delay} call. Using this macro requires
@code{lexical-binding}.
@end defmac
@defun thunk-force thunk
Force @var{thunk} to perform the evaluation of the forms specified in
the @code{thunk-delay} that created the thunk. The result of the
evaluation of the last form is returned. The @var{thunk} also
``remembers'' that it has been forced: Any further calls of
@code{thunk-force} with the same @var{thunk} will just return the same
result without evaluating the forms again.
@end defun
@defmac thunk-let (bindings@dots{}) forms@dots{}
This macro is analogous to @code{let} but creates ``lazy'' variable
bindings. Any binding has the form @w{@code{(@var{symbol}
@var{value-form})}}. Unlike @code{let}, the evaluation of any
@var{value-form} is deferred until the binding of the according
@var{symbol} is used for the first time when evaluating the
@var{forms}. Any @var{value-form} is evaluated at most once. Using
this macro requires @code{lexical-binding}.
@end defmac
Example:
@example
@group
(defun f (number)
(thunk-let ((derived-number
(progn (message "Calculating 1 plus 2 times %d" number)
(1+ (* 2 number)))))
(if (> number 10)
derived-number
number)))
@end group
@group
(f 5)
@result{} 5
@end group
@group
(f 12)
@print{} Calculating 1 plus 2 times 12
@result{} 25
@end group
@end example
Because of the special nature of lazily bound variables, it is an error
to set them (e.g.@: with @code{setq}).
@defmac thunk-let* (bindings@dots{}) forms@dots{}
This is like @code{thunk-let} but any expression in @var{bindings} is allowed
to refer to preceding bindings in this @code{thunk-let*} form. Using
this macro requires @code{lexical-binding}.
@end defmac
@example
@group
(thunk-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))
@print{} Calculating z...
@print{} Calculating y...
@print{} Calculating x...
@print{} Finished calculating x
@print{} Finished calculating y
@print{} Finished calculating z
@result{} 8
@end group
@end example
@code{thunk-let} and @code{thunk-let*} use thunks implicitly: their
expansion creates helper symbols and binds them to thunks wrapping the
binding expressions. All references to the original variables in the
body @var{forms} are then replaced by an expression that calls
@code{thunk-force} with the according helper variable as the argument.
So, any code using @code{thunk-let} or @code{thunk-let*} could be
rewritten to use thunks, but in many cases using these macros results
in nicer code than using thunks explicitly.
......@@ -109,6 +109,10 @@ Snake and Pong are more playable on HiDPI displays.
*** Completing filenames in the minibuffer via 'C-TAB' now uses the
styles as configured by the variable 'completion-styles'.
** New macros 'thunk-let' and 'thunk-let*'.
These macros are analogue to 'let' and 'let*', but create bindings that
are evaluated lazily.
* New Modes and Packages in Emacs 27.1
......
......@@ -41,6 +41,10 @@
;; following:
;;
;; (thunk-force delayed)
;;
;; This file also defines macros `thunk-let' and `thunk-let*' that are
;; analogous to `let' and `let*' but provide lazy evaluation of
;; bindings by using thunks implicitly (i.e. in the expansion).
;;; Code:
......@@ -71,5 +75,60 @@ with the same DELAYED argument."
"Return non-nil if DELAYED has been evaluated."
(funcall delayed t))
(defmacro thunk-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 when evaluating the BODY.
It is not allowed to set `thunk-let' or `thunk-let*' bound
variables.
Using `thunk-let' and `thunk-let*' requires `lexical-binding'."
(declare (indent 1) (debug let))
(cl-callf2 mapcar
(lambda (binding)
(pcase binding
(`(,(pred symbolp) ,_) binding)
(_ (signal 'error (cons "Bad binding in thunk-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 thunk-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 when evaluating the BODY.
It is not allowed to set `thunk-let' or `thunk-let*' bound
variables.
Using `thunk-let' and `thunk-let*' requires `lexical-binding'."
(declare (indent 1) (debug let))
(cl-reduce
(lambda (expr binding) `(thunk-let (,binding) ,expr))
(nreverse bindings)
:initial-value (macroexp-progn body)))
;; (defalias 'lazy-let #'thunk-let)
;; (defalias 'lazy-let* #'thunk-let*)
(provide 'thunk)
;;; thunk.el ends here
......@@ -51,5 +51,55 @@
(thunk-force thunk)
(should (= x 1))))
;; thunk-let tests
(ert-deftest thunk-let-basic-test ()
"Test whether bindings are established."
(should (equal (thunk-let ((x 1) (y 2)) (+ x y)) 3)))
(ert-deftest thunk-let*-basic-test ()
"Test whether bindings are established."
(should (equal (thunk-let* ((x 1) (y (+ 1 x))) (+ x y)) 3)))
(ert-deftest thunk-let-bound-vars-cant-be-set-test ()
"Test whether setting a `thunk-let' bound variable fails."
(should-error
(eval '(thunk-let ((x 1)) (let ((y 7)) (setq x (+ x y)) (* 10 x))) t)))
(ert-deftest thunk-let-laziness-test ()
"Test laziness of `thunk-let'."
(should
(equal (let ((x-evalled nil)
(y-evalled nil))
(thunk-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 thunk-let*-laziness-test ()
"Test laziness of `thunk-let*'."
(should
(equal (let ((x-evalled nil)
(y-evalled nil)
(z-evalled nil)
(a-evalled nil))
(thunk-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 thunk-let-bad-binding-test ()
"Test whether a bad binding causes an error when expanding."
(should-error (macroexpand '(thunk-let ((x 1 1)) x)))
(should-error (macroexpand '(thunk-let (27) x)))
(should-error (macroexpand '(thunk-let x x))))
(provide 'thunk-tests)
;;; thunk-tests.el ends here
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment