async/await for Emacs Lisp
aiois to Emacs Lisp as
asynciois to Python. This package builds upon Emacs 25 generators to provide functions that pause while they wait on asynchronous events. They do not block any thread while paused.
Introduction: An Async / Await Library for Emacs Lisp
Installation is available through MELPA. Since it uses the
recordbuilt-in, it requires Emacs 26 or later.
An async function is defined using
aio-lambda. The body of such functions can use
aio-awaitto pause the function and wait on a given promise. The function continues with the promise's resolved value when it's ready. The package provides a number of functions that return promises, and every async function returns a promise representing its future return value.
(aio-defun foo (url) (aio-await (aio-sleep 3)) (message "Done sleeping. Now fetching %s" url) (let* ((result (aio-await (aio-url-retrieve url))) (contents (with-current-buffer (cdr result) (prog1 (buffer-string) (kill-buffer))))) (message "Result: %s" contents)))
If an uncaught signal terminates an asynchronous function, that signal is captured by its return value promise and propagated into any function that awaits on that function.
(aio-defun divide (a b) (aio-await (aio-sleep 1)) (/ a b))
(aio-defun divide-safe (a b) (condition-case error (aio-await (divide a b)) (arith-error :arith-error)))
(aio-wait-for (divide-safe 1.0 2.0)) ;; => 0.5
(aio-wait-for (divide-safe 0 0)) ;; => :arith-error
To convert a callback-based function into an awaitable, async-friendly function, create a new promise object with
aio-resolvethat promise in the callback. The helper function,
aio-make-callback, makes this easy.
(aio-wait-for promise) ;; Synchronously wait for PROMISE, blocking the current thread.
(aio-cancel promise) ;; Attempt to cancel PROMISE, returning non-nil if successful.
(aio-with-promise promise &rest body) [macro] ;; Evaluate BODY and resolve PROMISE with the result.
(aio-with-async &rest body) [macro] ;; Evaluate BODY asynchronously as if it was inside `aio-lambda'.
(aio-make-callback &key tag once) ;; Return a new callback function and its first promise.
(aio-chain expr) [macro] ;; `aio-await' on EXPR and replace place EXPR with the next promise.
aio-make-callbackfunction is especially useful for callbacks that are invoked repeatedly, such as process filters and sentinels. The
aio-chainmacro works in conjunction.
Here are some useful promise-returning — i.e. awaitable — functions defined by this package.
(aio-sleep seconds &optional result) ;; Return a promise that is resolved after SECONDS with RESULT.
(aio-idle seconds &optional result) ;; Return a promise that is resolved after idle SECONDS with RESULT.
(aio-url-retrieve url &optional silent inhibit-cookies) ;; Wraps `url-retrieve' in a promise.
(aio-all promises) ;; Return a promise that resolves when all PROMISES are resolved."
This package includes a select()-like, level-triggered API for waiting on multiple promises at once. Create a "select" object, add promises to it, and await on it. Resolved and returned promises are automatically removed, and the "select" object can be reused.
(aio-make-select &optional promises) ;; Create a new `aio-select' object for waiting on multiple promises.
(aio-select-add select promise) ;; Add PROMISE to the set of promises in SELECT.
(aio-select-remove select promise) ;; Remove PROMISE form the set of promises in SELECT.
(aio-select-promises select) ;; Return a list of promises in SELECT.
(aio-select select) ;; Return a promise that resolves when any promise in SELECT resolves.
For example, here's an implementation of sleep sort:
(aio-defun sleep-sort (values) (let* ((promises (mapcar (lambda (v) (aio-sleep v v)) values)) (select (aio-make-select promises))) (cl-loop repeat (length promises) for next = (aio-await (aio-select select)) collect (aio-await next))))
Semaphores work just as they would as a thread synchronization primitive. There's an internal counter that cannot drop below zero, and
aio-sem-waitis an awaitable function that may block the asynchronous function until another asynchronous function calls
aio-sem-post. Blocked functions wait in a FIFO queue and are awoken in the same order that they awaited.
(aio-sem init) ;; Create a new semaphore with initial value INIT.
(aio-sem-post sem) ;; Increment the value of SEM.
(aio-sem-wait sem) ;; Decrement the value of SEM.
This can be used to create a work queue. For example, here's a configurable download queue for
(defun fetch (url-list max-parallel callback) (let ((sem (aio-sem max-parallel))) (dolist (url url-list) (aio-with-async (aio-await (aio-sem-wait sem)) (cl-destructuring-bind (status . buffer) (aio-await (aio-url-retrieve url)) (aio-sem-post sem) (funcall callback (with-current-buffer buffer (prog1 (buffer-string) (kill-buffer)))))))))