branch: externals/futur
commit d9fdf794be9bd93e7d0a98cc56f867b6887f13f8
Author: Stefan Monnier <[email protected]>
Commit: Stefan Monnier <[email protected]>
Improve doc, provide `make test` target
* Makefile: New file.
* futur.el: Beef up commentary.
(futur--done, futur--error, futur--waiting): Mark the Pcase macros as
internal.
(futur-send-file): Provide a trivial implementation.
(futur-blocker-abort) <futur>: Fix thinko and make it run the aux functions.
(futur-blocker-abort) <futurs>: Simplify now that we fixed the thinko.
* futur-tests.el: Require `ert` (not sure why, but I get `void-function
(ert-set-test)` otherwise).
---
Makefile | 7 +++
futur-tests.el | 13 ++--
futur.el | 190 +++++++++++++++++++++++++++++++++++++++++----------------
3 files changed, 151 insertions(+), 59 deletions(-)
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..7c9cd87c23
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+# Not much to see here.
+
+EMACSBIN = emacs
+EMACS = $(EMACSBIN) --batch
+
+test:
+ $(EMACS) -L . -l futur-tests -f ert-run-tests-batch
diff --git a/futur-tests.el b/futur-tests.el
index 6bd93a623a..f02abccd88 100644
--- a/futur-tests.el
+++ b/futur-tests.el
@@ -25,6 +25,7 @@
;;; Code:
(require 'futur)
+(require 'ert)
(ert-deftest futur--resignal ()
(let ((err1 (list 'error "hello")))
@@ -75,7 +76,7 @@
(ert-deftest futur-abort ()
(let* ((x '())
(start (float-time))
- (timescale 0.2)
+ (timescale 0.5)
(_fut1 (futur-let* ((_ <- (futur-timeout (* timescale 1))))
(push 'timer1 x)))
(fut6 (futur-let* ((_ <- (futur-timeout (* timescale 6))))
@@ -99,11 +100,11 @@
'(error "" timer1)))
(should (equal x '(timer1)))
(should (< (- (float-time) start) (* timescale 4)))
- (should (pcase fut6 ((futur-waiting _) t)))
- (should (pcase fut2 ((futur-done 'nil) t)))
- (should (pcase fut22 ((futur-error '(futur-aborted)) t)))
- (should (pcase fut4 ((futur-waiting _) t) ((futur-done 'nil) t)))
- (should (pcase futB ((futur-waiting _) t)))
+ (should (pcase fut6 ((futur--waiting _) t)))
+ (should (pcase fut2 ((futur--done 'nil) t)))
+ (should (pcase fut22 ((futur--error '(futur-aborted)) t)))
+ (should (pcase fut4 ((futur--waiting _) t) ((futur--done 'nil) t)))
+ (should (pcase futB ((futur--waiting _) t)))
(should (equal '(nil) (futur-blocking-wait-to-get-result futB)))
(should (equal x '(timer1)))))
diff --git a/futur.el b/futur.el
index b7cd1201af..897efca5fa 100644
--- a/futur.el
+++ b/futur.el
@@ -31,22 +31,99 @@
;; which can be used as follows:
;; (futur-let*
-;; ((cmd (build-arg-list))
-;; (exitcode <- (futur-process-make :command cmd :buffer t))
-;; (out (buffer-string)) ;; Get the process's output.
-;; (cmd2 (build-second-arg-list exitcode out))
-;; (otherexit <- (futur-process-make :command cmd :buffer t)))
-;; (buffer-string))
+;; ((buf (current-buffer))
+;; (exitcode1 <- (futur-process-call CMD1 nil buf nil ARG1 ARG2))
+;; (out (with-current-buffer buf
+;; (buffer-string))) ;; Get the process's output.
+;; (exitcode2 <- (futur-process-call CMD2 nil buf nil ARG3 ARG4)))
+;; (with-current-buffer buf
+;; (buffer-string)))
;; This example builds a future which runs two commands in sequence.
;; For those rare cases where you really do need to block everything
;; else and wait for a future to complete, you can
;; use`futur-blocking-wait-to-get-result'.
-;; New kinds of futures can be constructed from:
-;; - `futur-waiting' to create the actual future.
-;; - `futur-deliver-value' to deliver the value to the future created earlier
-;; with `futur-waiting'.
+;;;; Low level API
+
+;; - (futur-done VAL) to create a trivial future returning VAL.
+;; - (futur-error ERR) to create a trivial failed future.
+;; - (futur-new FUN) to create a non-trivial future.
+;; FUN is called with one argument (the new `futur' object) and should
+;; return the "blocker" that `futur' is waiting for (used mostly
+;; when aborting a future).
+;; - (futur-abort FUTUR): Aborts execution of FUTUR.
+;; - (futur-deliver-value FUTUR VAL): Mark FUTUR as having completed
+;; successfully with VAL, and runs the clients waiting for that event.
+;; - (futur-deliver-failure FUTUR ERROR): Mark FUTUR as having failed
+;; with ERROR, and runs the clients waiting for that event.
+;; - (futur-register-callback FUTUR FUN): Register FUN as a client.
+;; Will be called with two arg (the ERROR and the VAL) when FUTURE completes.
+;; - (futur-blocking-wait-to-get-result FUTUR): Busy-wait for FUTUR to complete
+;; and return its value. Better use `futur-bind' or `futur-let*' instead.
+;; BEWARE: Please don't use it unless you really absolutely have to.
+
+;;;; Composing futures
+
+;; - (futur-bind FUTUR FUN &optional ERROR-FUN): Builds a new future which
+;; waits for FUTUR to completes and then calls FUN (or ERROR-FUN) with the
+;; resulting value (or its error). (ERROR-)FUN should itself return
+;; a future, tho if it doesn't it's automatically turned into a trivial one.
+;; - (futur-let* BINDINGS [:error-fun ERROR-FUN] BODY): Macro built on top
+;; of `futur-bind' which runs BINDINGS in sequence and then runs BODY.
+;; Each BINDING can be either a simple (PAT EXP) that is executed
+;; as in a `pcase-let*' or a (PAT <- FUTUR) in which case the rest is
+;; delayed until FUTUR completes.
+;; - (futur-list &rest FUTURS): Run FUTURS concurrently and return the
+;; resulting list of values.
+
+;;;; Related packages
+
+;; - [deferred](https://melpa.org/#/deferred): Provides similar functionality.
+;; Maybe the only reason `futur.el' exists is because `deferred' is different
+;; from what I expected (NIH syndrome?).
+;; - [promise](https://melpa.org/#/promise) is a very similar library,
+;; which tries to stay as close as possible to JavaScript's promises,
+;; leading to a very non-idiomatic implementation in `promise-core.el'.
+;; TODO: We only provide the core functionality of `promise', currently
+;; and it would make sense to add most of the rest, or even provide
+;; a bridge between the two.
+;; - [pfuture](https://melpa.org/#/pfuture): Sounds similar, but is more
+;; of a wrapper around `make-process'. Compared to this package,
+;; `pfuture' does not try very hard to help compose async computations
+;; and to propagate errors.
+;; - [async](http://elpa.gnu.org/packages/async.html): A package that focuses
+;; on executing ELisp code concurrently by launching additional Emacs
+;; (batch) sessions.
+;; TODO: It would make a lot of sense to allow use of `async'
+;; via `futur' objects.
+;; - [async-await](https://melpa.org/#/async-await): This provides
+;; JavaScript-style async/await operators on top of the `promise' package.
+;; This fundamentally require a kind of CPS conversion of the code, for
+;; which they use `generator.el'.
+;; TODO: It would be possible to make `async-await' work on top of `futur',
+;; but to the extent that `generator.el' is not able to perform CPS
+;; correctly in all cases (because it's hard/impossible in general),
+;; I'm not sure it's a good idea to encourage this coding style.
+;; Maybe instead we should develop some way to detect&flag most of the
+;; pitfalls of the current style (such as using `progn' instead of
+;; `future-let*' to sequence execution when one part is a future).
+;; - [aio](https://melpa.org/#/aio): Also provides await/async style
+;; coding (also using `generator.el' under the hood) but using its
+;; own (much simpler) "promise" objects.
+;; - [async1](https://melpa.org/#/async1): A more limited/ad-hoc solution to
+;; the problem that async/await try to solve that hence avoids the need
+;; to perform CPS. Not sure if it's significantly better than `futur-let*'.
+;; - [asyncloop](https://melpa.org/#/asyncloop): Focuses on just
+;; running a sequence of function calls with regular "stops" in-between
+;; to let other operations happen "concurrently".
+;; - [async-job-queue](https://melpa.org/#/async-job-queue):
+;; - [pdd](https://melpa.org/#/pdd): HTTP library that uses its own
+;; implementation of promises.
+
+;;; News:
+
+;; 2026: After years of sitting in the dark, it's finally getting dusted up.
;;; Code:
@@ -141,23 +218,23 @@ A futur has 3 possible states:
(clients nil)
(value nil))
-(pcase-defmacro futur-done (result)
+(pcase-defmacro futur--done (result)
`(and (pred futur--p)
(app futur--clients 't)
(app futur--value ,result)))
-(pcase-defmacro futur-error (error-object)
+(pcase-defmacro futur--error (error-object)
`(and (pred futur--p)
(app futur--clients 'error)
(app futur--value ,error-object)))
-(pcase-defmacro futur-waiting (&optional blocker clients)
+(pcase-defmacro futur--waiting (&optional blocker clients)
`(and (pred futur--p)
(app futur--clients (and (pred listp) ,(or clients '_)))
(app futur--value ,(or blocker '_))))
(defun futur--waiting-p (futur)
- (pcase futur ((or (futur-waiting)
+ (pcase futur ((or (futur--waiting)
;; Tell Pcase to presume FUTUR *is* a futur.
(and (pred (not futur--p)) pcase--dontcare))
t)))
@@ -170,7 +247,7 @@ A futur has 3 possible states:
(defun futur--deliver (futur err val)
(pcase-exhaustive futur
- ((futur-waiting _ clients)
+ ((futur--waiting _ clients)
(setf (futur--clients futur) (if err 'error t))
(setf (futur--value futur) (or err val))
;; CLIENTS is usually in reverse order since we always `push' to them.
@@ -216,7 +293,7 @@ The object can be any object for which there is a
`futur-blocker-wait' method."
"Interrupt execution of FUTUR, marking it as having failed.
The error is `futur-aborted'. Does nothing if FUTUR was already complete."
(pcase futur
- ((futur-waiting blocker)
+ ((futur--waiting blocker)
(let ((error (list 'futur-aborted)))
(futur-blocker-abort blocker error)
(futur-deliver-failure futur error)))
@@ -232,10 +309,10 @@ otherwise ERR is nil and VAL is the result value.
When FUN is called, FUTUR is already marked as completed.
If FUTUR already completed, FUN is called immediately."
(pcase futur
- ((futur-waiting _ clients)
+ ((futur--waiting _ clients)
(setf (futur--clients futur) (cons fun clients)))
- ((futur-error err) (funcall fun err nil))
- ((futur-done val) (funcall fun nil val)))
+ ((futur--error err) (funcall fun err nil))
+ ((futur--done val) (funcall fun nil val)))
nil)
(defun futur-ize (val)
@@ -260,7 +337,7 @@ By default any error in FUTUR is propagated to the returned
future."
;;
;; But we try to skip the `new' futur if `futur' is already completed.
(pcase-exhaustive futur
- ((futur-waiting _ clients)
+ ((futur--waiting _ clients)
(let ((new (futur--waiting futur)))
(setf (futur--clients futur)
(cons
@@ -269,8 +346,8 @@ By default any error in FUTUR is propagated to the returned
future."
(futur--run-continuation new fun (list val))))
clients))
new))
- ((and (futur-error _) (guard (null error-fun))) futur)
- ((or (futur-done value) (futur-error err1))
+ ((and (futur--error _) (guard (null error-fun))) futur)
+ ((or (futur--done value) (futur--error err1))
(condition-case-unless-debug err2
(let ((res (if err1 (funcall error-fun err1) (funcall fun value))))
(futur-ize res))
@@ -326,8 +403,8 @@ its result, or (re)signals the error if ERROR-FUN is nil."
(condition-notify condition))))
(condition-wait condition))))
(pcase-exhaustive futur
- ((futur-error err) (funcall (or error-fun #'futur--resignal) err))
- ((futur-done val) val)))
+ ((futur--error err) (funcall (or error-fun #'futur--resignal) err))
+ ((futur--done val) val)))
(defmacro futur-let* (bindings &rest body)
"Sequence asynchronous operations via futures.
@@ -359,7 +436,12 @@ ERROR-FUN is called with a single argument, the error
object."
,error-fun)))))))
(oclosure-define futur--aux
- "An auxiliary function used internally. Does not need the future's
completion.")
+ "An auxiliary function used internally.
+When used as a callback in a future, a function of type `futur--aux' differs
+from other functions in that it means it does not need the future's result
+nearly as much as the future itself needs this function.
+Concretely what it means is that it is OK to abort a future whose only
+clients are `futur--aux' functions.")
(defun futur--multi-clients-p (clients)
(let ((count 0))
@@ -412,7 +494,7 @@ Return non-nil if we successfully waited until the
completion of BLOCKER."
(let ((i 0))
(while
(pcase futur
- ((futur-waiting blocker)
+ ((futur--waiting blocker)
(if (futur-blocker-wait blocker)
(setq i 0)
(let ((delay (* 0.01 (expt 1.1 i))))
@@ -433,20 +515,25 @@ Return non-nil if we successfully waited until the
completion of BLOCKER."
If it had not been computed yet, then make it fail with ERROR.")
(cl-defmethod futur-blocker-abort ((futur futur) error)
- (let ((blocker (futur--blocker futur)))
- (if (pcase blocker ((futur-waiting _ (pred futur--multi-clients-p)) t))
- ;; If there are more than 1 clients, presumably someone else is
- ;; still interested in FUTURs result, so we shouldn't abort it.
- ;; FIXME: We should "unbind" ourselves from it, tho, otherwise
- ;; when it completes it will deliver its result to us.
- nil
- ;; If CLIENTS has only one element, it's presumably ourselves,
- ;; so we should definitely abort that futur.
- (futur-blocker-abort blocker error))
- ;; Regardless if we aborted the blocker, abort this future,
- ;; but don't "deliver" since our caller should take care of it.
- (setf (futur--clients futur) 'error)
- (setf (futur--value futur) error)))
+ (pcase futur
+ ((futur--waiting _ (pred futur--multi-clients-p))
+ ;; If there are more than 1 clients, presumably someone else is
+ ;; still interested in FUTURs result, so we shouldn't abort it.
+ ;; FIXME: We should "unbind" ourselves from it, tho, otherwise
+ ;; when it completes it will deliver its result to us.
+ nil)
+ ((futur--waiting blocker clients)
+ ;; If CLIENTS has only one "real" element, it's presumably the future
+ ;; we're in the process of aborting (call it CHILD), so there's
+ ;; no harm in aborting FUTUR. We should not just `futur-abort'
+ ;; FUTUR because we shouldn't run CHILD's client, but we should
+ ;; still run the other (auxiliary/cleanup) functions.
+ (futur-blocker-abort blocker error)
+ (setf (futur--clients futur) 'error)
+ (setf (futur--value futur) error)
+ (dolist (client clients)
+ (when (cl-typep client 'futur--aux)
+ (futur--funcall client error nil))))))
(cl-defmethod futur-blocker-abort ((_ (eql 'elisp)) _error)
;; FIXME: No idea how to do that!
@@ -507,6 +594,13 @@ The ARGS are like those of `make-process' except that they
can't include
(let* ((file (process-get proc 'futur-destination)))
(write-region string nil file 'append 'silent)))
+(defun futur-send-file (proc infile)
+ ;; FIXME: Make it more concurrent!
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally infile)
+ (futur-process-send proc (buffer-string))))
+
(defun futur-process-call (program &optional infile destination _display
&rest args)
"Like `call-process' but runs concurrently as a `futur'.
@@ -528,7 +622,7 @@ The DISPLAY argument is ignored: redisplay always happens."
:buffer (if (bufferp destination) destination)
:filter (if (bufferp destination) nil
#'futur-process-call--filter)))
- (proc (pcase-exhaustive futur ((futur-waiting blocker) blocker))))
+ (proc (pcase-exhaustive futur ((futur--waiting blocker) blocker))))
(when (stringp destination)
(write-region "" nil destination nil 'silent))
(pcase-exhaustive infile
@@ -614,7 +708,7 @@ that have not yet completed."
(pcase new
;; We don't unbind ourselves from some FUTURs
;; when aborting, so ignore their delivery here.
- ((futur-error '(futur-aborted)) nil)
+ ((futur--error '(futur-aborted)) nil)
(_ (futur-deliver-value new args)))))))))
(setq i (1+ i)))
new)))
@@ -634,17 +728,7 @@ that have not yet completed."
(cl-defmethod futur-blocker-abort ((futurs cons) error)
;; Propagate the abort to the futurs we're still waiting for.
(dolist (futur futurs)
- (pcase futur
- ((futur-waiting _ clients)
- (if (futur--multi-clients-p clients)
- ;; If there are more than 1 clients, presumably someone else is
- ;; still interested in FUTURs result, so we shouldn't abort it.
- ;; FIXME: We should "unbind" ourselves from it, tho, otherwise
- ;; when it completes it will deliver its result to us.
- nil
- ;; If CLIENTS has only one element, it's presumably ourselves,
- ;; so we should definitely abort that futur.
- (futur-blocker-abort futur error))))))
+ (futur-blocker-abort futur error)))
;;;; Other helpers