Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return to monke #4

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
660 changes: 0 additions & 660 deletions LICENSE.md

This file was deleted.

63 changes: 22 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,42 @@
# CL-ASYNC-AWAIT

This library allows you to have async functions similar to those
in JavaScript, only these async functions are implemented with
threads instead of an event dispatching mechanism.
in JavaScript.

Cross-thread error handling is easy using `CL-ASYNC-AWAIT`, since
the `AWAIT` operator propagates errors from the promise, and also
propagates invoked restarts back to the promise.
Example:

## Simple example

```
(defun-async my-async-reader (stream)
(read-byte stream))

(defvar *promise* (my-async-reader *some-stream*))
(defvar *my-byte* (await *promise*))
```
(defun-async :delay example-function (p1 p2)
(async-handler-case
(await-let* ((x p1)
(y p2))
(/ x y))
(division-by-zero (db0) (format *error-output* "Caught error: ~a~%" db0))))

## Usage
(defun-async promise-value (n) n)

(defvar *p1* (example-function (promise-value 1) (promise-value 2)))
(defvar *p2* (example-function (promise-value 1) (promise-value 0)))

```
(lambda-async lambda-list &body body)
```

Creates a `CL:LAMBDA` function that creates a `PROMISE` when FUNCALLed.
Since Common Lisp is not a fully asynchronous language built around PROMISE objects,
it is necessary for the programmer to decide when each PROMISE will be FORCEd.

By default, DEFUN-ASYNC and LAMBDA-ASYNC will create IMMEDIATE-PROMISEs, which
resolve as soon as they are created. Delayed promises can be created with
the :DELAY keyword, whose usage is shown for both DEFUN-ASYNC and
LAMBDA-ASYNC below:

```
(defun-async name lambda-list &body body)
(defun-async :delay function-name lambda-list &body body)
(lambda-async :delay lambda-list &body body)
```

Like `LAMBDA-ASYNC` but expands to a `CL:DEFUN` form instead of a `CL:LAMBDA` form.
Delayed promises will execute if and only if the FORCE method is invoked on them:

```
(await promise)
(force promise)
```

Wait for a `PROMISE` to resolve to one or more values. If the promise
succeeds, the values will be returned using `CL:VALUES`.

If an error occurs in the `PROMISE` thread and is not handled within the
promise, execution of the `PROMISE` thread is suspended until the `AWAIT`
method is called.

That error will then be signalled in the thread from which `AWAIT` is called, in
a context where all the same restarts are defined as are defined in the `PROMISE`
thread. If `INVOKE-RESTART` is called with one of these restarts, that restart
will be invoked in the `PROMISE` thread, and `AWAIT` will return that restart's value form.

If the stack frame for the call to `AWAIT` is unwound without invoking a restart,
the `PROMISE` thread will invoke its `CL:ABORT` restart.

Whether the `PROMISE` succeeds or fails, the result is memoized. Calling `AWAIT` a second time
on the same `PROMISE` will yield the same values.

If an error occurred and `AWAIT` is called a second time, the same error will be signalled, but
the restarts will not be available, since the `PROMISE` thread is expected to be dead as a result
of invoking the `ABORT` restart.

99 changes: 99 additions & 0 deletions async-functions.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
(in-package :cl-async-await)

(defmacro with-declare-form (var function-body &body body)
`(let ((,var (if (and (consp (car ,function-body))
(eq (caar ,function-body) 'declare))
(list (pop ,function-body)))))
,@body))

(defmacro lambda-async (lambda-list &body body)
"Usage:

(lambda-async lambda-list &body body) or
(lambda-async :kw lambda-list &body body)

Expands to a CL:LAMBDA that returns a PROMISE when FUNCALLed.

The PROMISE resolves to whatever the BODY returns.

The :KW parameter, if provided, must be replaced with either :DELAY or
:IMMEDIATE. If :DELAY is given, the lambda will return a PROMISE
object which won't execute until its value is requested (ie, until it is
FORCEd).

If :IMMEDIATE is given, the BODY is executed synchronously, and an
already-resolved promise will be returned.

If no :KW argument is given, then a PARALLEL-PROMISE is created. Its
code will execute immediately in a new thread.
"
(let ((promise-type (cond ((eq lambda-list :delay)
(setf lambda-list (pop body))
'promise)
((eq lambda-list :immediate)
(setf lambda-list (pop body))
'immediate-promise)
(t 'parallel-promise))))
(with-declare-form declare-form body
`(lambda ,lambda-list
(make-instance ',promise-type
:thunk (lambda (resolve)
,@declare-form
(apply resolve
(multiple-value-list
(progn ,@body)))))))))

(defmacro defun-async (name lambda-list &body body)
"Just like LAMBDA-ASYNC, except it expands to a CL:DEFUN form instead of CL:LAMBDA."
(let ((promise-type (cond ((eq name :delay)
(setf name lambda-list)
(setf lambda-list (pop body))
'promise)
((eq name :immediate)
(setf name lambda-list)
(setf lambda-list (pop body))
'immediate-promise)
(t 'parallel-promise))))
(with-declare-form declare-form body
`(defun ,name ,lambda-list
,@declare-form
(make-instance ',promise-type
:thunk (lambda (resolve)
(apply resolve
(multiple-value-list
(block ,name
,@body)))))))))

(defmacro await-let1 ((var promise) &body body)
(alexandria:with-gensyms (ignored-values)
`(then ,promise
(lambda (,var &rest ,ignored-values)
(declare (ignore ,ignored-values))
,@body))))

(defmacro await-let* (bindings &body body)
"Sequentially bind variables to different promises."
(unless bindings
(error "AWAIT-LET* with null BINDINGS is disallowed."))
`(await-let1 ,(car bindings)
,@(if (cdr bindings)
(list `(await-let* ,(cdr bindings) ,@body))
body)))

(defmacro await-multiple-value-bind (lambda-list promise &body body)
"Bind the multiple values returned by the PROMISE to a lambda-list."
`(then ,promise
(lambda ,lambda-list ,@body)))

(defmacro async-handler-case (form &body cases)
"Behaves just like CL:HANDLER-CASE, except if the FORM evals to a CL-ASYNC-AWAIT:PROMISE,
it will attach error handlers to the promise and then return it."
(let ((promise (gensym)))
`(handler-case
(let ((,promise ,form))
,@(loop for (condition-type lambda-list . body) in cases
collect `(catch-exception ,promise ',condition-type
(lambda ,lambda-list ,@body)))
,promise)
,@cases)))

3 changes: 2 additions & 1 deletion cl-async-await.asd
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
:version "1"
:license "AGPLv3"
:description "An implementation of async/await for Common Lisp"
:depends-on (:closer-mop :bordeaux-threads :simple-actors)
:depends-on (:closer-mop :bordeaux-threads)
:components
((:file "package")
(:file "utils" :depends-on ("package"))
(:file "async-functions" :depends-on ("package" "promise" "utils"))
(:file "promise" :depends-on ("package" "utils"))))
3 changes: 1 addition & 2 deletions package.lisp
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
(cl:defpackage :cl-async-await
(:use :closer-common-lisp :bordeaux-threads :simple-actors/ipc)
(:export :await :defun-async :lambda-async :promise)
(:use :closer-common-lisp :bordeaux-threads)
(:shadow assoc))
Loading