async/await for continuation-passing style functions
Continuation-passing style (CPS) is a pattern where a function, instead of returning a value, calls the provided continuation function with that value. It's used in popular Clojure libraries (Ring's async handlers, clj-http) to avoid blocking the calling thread by long-running IO operations.
Writing correct CPS code is awkward and hard to get right however. Any non-trivial flow can quickly become unmanageable. This library delivers async/await expressions that let you write idiomatic, synchronous-looking code while leveraging the power of asynchronous, continuation-passing style functions.
await
the execution of asynchronous function in an async
block just like
it was a blocking function call.
(require '[await-cps :refer [async await]]
'[clj-http.client :as http])
(defn star-wars-greeting-handler [request respond raise]
; initiate the async block
(async respond raise
(let [person-url (str "https://swapi.co/api/people/" (:id (:params request)))
; await the completion of asynchronous http request, doesn't block the thread
person (:body (await http/get person-url {:async? true :as :json}))]
(str "Hi! I'm " (:name person) " from "
; await expression can go wherever a function call is allowed
(get-in (await http/get (:homeworld person) {:async? true :as :json})
[:body :name])))))
Use defn-async
for brevity.
(defn-async star-wars-greeting-handler [request]
(let [person-url (str "https://swapi.co/api/people/" (:id (:params request)))
person (:body (await http/get person-url {:async? true :as :json}))]
...))
Asynchronous function is any function that takes resolve
and raise
callbacks
as the last two parameters. It is expected to evetually either call resolve
with the result value, call raise
with a Throwable
or throw in the calling
thread. The return value is ignored.
You can await the completion of asynchronous function in an async
block
calling await
with the function and any parameters, leaving the callbacks
out. You're free to use await wherever a function call is allowed.
The execution does not block the calling thread, resuming in whatever
thread the awaited function invokes the callback in instead. Awaiting will
observe the resolved value as if it was returned or any exception thrown or
raised as if it was thrown.
async
body can handle arbitrary Clojure code. Its boundary does not stretch
inside nested def
s nor the body of any nested function however.
This includes def
, fn
, reify
, deftype
and functions in letfn
.
The code below does not work:
(async resolve raise
(doall (map (fn [url] (await http/get url {:async true}))
["https://google.com" "https://twitter.com"])))
=> IllegalStateException await called outside async block
Use loop/recur
to traverse collections.
(async resolve raise
(loop [[url & urls] ["https://google.com" "https://twitter.com"]]
(when url
(println (:body (await http/get url {:async? true})))
(recur urls))))
You can also use fn-async
for ad-hoc asynchronous functions.
(async resolve raise
(loop [[f & fs] (map (fn [url] (fn-async []
(:body (await http/get url {:async? true}))))
["https://google.com" "https://twitter.com"])]
(when f
(println (await f))
(recur fs))))
Recurring is supported in the context of fn-async
, defn-async
and loop
within async
block.
try/catch/finally
is fully supported. Note however that if a CPS function fails
to call either the resolve or raise callback the finally
block may never execute.
This would be equivalent to
killing a thread
that's executing a regular try
block.
monitor-enter
and monitor-exit
(and by extension the locking
macro)
are JVM's low level concurrency primitives strictly bound to executing thread
and therefore are not supported in async
blocks.
Used across asynchronous call will lead to concurrency bugs.
Currently there's no warning if this is to happen.
Being cautious about third-party software applying chainsaw surgery to your production code is only fair. The goal of this library is for you to be able to use it with confidence.
The test suite included employs generative testing producing nested combinations of expressions, including special forms, synchronous and asynchronous function calls, failures and side-effects. It asserts that both the result (value returned or exception thrown) and the order of any side-effects is consistent with what you'd observe executing synchronously in a single thread.
At the same time, the project has not seen extensive production use yet, use with caution. Please, raise any issues through GitHub.
Even though a bit awkward, a CPS implementation of the happy path tends to be straightforward enough. Covering exceptional cases, however, is a whole lot harder.
Any exceptions not handled by resolve
callback will likely
get swallowed.
Just to be safe you should wrap all your resovle
functions in a catch-all
calling raise
Writing correct CPS equivalent for try/catch/finally
block is about
the trickiest problem this library solves. It needs to:
resolve
callbackThe amount of edge cases is exactly what inspired the use of generative testing in this library. I strongly recommend avoiding roll-your-own solutions without thorough coverage.
Even if you are confident that you can get an equivalent for try/catch/finally
right, any try/finally
facilities (like with-open
, binding
and others)
won't ever work across asynchronous calls without transforming the macroexpanded
form.
This project is distributed under The MIT License.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close