kitchen-async
A Promise library for ClojureScript, or a poor man's core.async
Features
- syntactic support for writing asynchronous code handling Promises as easily as with
async/await
in ECMAScript - also available on self-hosted ClojureScript environments, such as Lumo/Planck
- seamless (opt-in) integration with core.async channels
kitchen-async focuses on the ease of Promise handling, and is not specifically intended to be so much performant. If you would rather like such a library, promesa or core.async might be more suitable.
Example
Assume you are writing some Promise
-heavy async code in ClojureScript (e.g. Google's Puppeteer provides such a collection of APIs). Then, if you only use raw JavaScript interop facilities for it, you would have to write something like this:
(def puppeteer (js/require "puppeteer"))
(-> (.launch puppeteer)
(.then (fn [browser]
(-> (.newPage browser)
(.then (fn [page]
(-> (.goto page "https://clojure.org")
(.then #(.screenshot page #js{:path "screenshot.png"}))
(.catch js/console.error)
(.then #(.close browser)))))))))
kitchen-async
provides more succinct, "direct style" syntactic sugar for those things, which you may find similar to async/await
in ECMAScript 2017:
(require '[kitchen-async.promise :as p])
(def puppeteer (js/require "puppeteer"))
(p/let [browser (.launch puppeteer)
page (.newPage browser)]
(p/try
(.goto page "https://clojure.org")
(.screenshot page #js{:path "screenshot.png"})
(p/catch :default e
(js/console.error e))
(p/finally
(.close browser))))
Installation
Add the following to your :dependencies
:
Or, if you'd rather use an unstable version of the library, you can do that easily via deps.edn
as well:
athos/kitchen-async {:git/url "https://github.com/athos/kitchen-async.git" :sha <commit sha hash>}
Usage
kitchen-async provides two major categories of APIs:
You can use all these APIs once you require
kitchen-async.promise
ns, like the following:
(require '[kitchen-async.promise :as p])
Thin wrapper APIs for JS Promise
p/promise
macro
* p/promise
macro creates a new Promise:
(p/promise [resolve reject]
(js/setTimeout #(resolve 42) 1000))
;=> #object[Promise [object Promise]]
This code is equivalent to:
(js/Promise.
(fn [resolve reject]
(js/setTimeout #(resolve 42) 1000)))
p/then
& p/catch*
* p/then
and p/catch*
simply wrap Promise's .then
and .catch
methods, respectively. For example:
(-> (some-promise-fn)
(p/then (fn [x] (js/console.log x)))
(p/catch* (fn [err] (js/console.error err))))
is almost equivalent to:
(-> (some-promise-fn)
(.then (fn [x] (js/console.log x)))
(.catch (fn [err] (js/console.error err))))
p/resolve
& p/reject
* p/resolve
and p/reject
wraps Promise.resolve
and Promise.reject
, respectively. For example:
(p/then (p/resolve 42) prn)
is equivalent to:
(.then (js/Promise.resolve 42) prn)
p/all
& p/race
* p/all
and p/race
wraps Promise.all
and Promise.race
, respectively. For example:
(p/then (p/all [(p/resolve 21)
(p/promise [resolve]
(js/setTimeout #(resolve 21) 1000))])
(fn [[x y]] (prn (+ x y))))
is almost equivalent to:
(.then (js/Promise.all #js[(js/Promise.resolve 42)
(js/Promise.
(fn [resolve]
(js/setTimeout #(resolve 42) 1000)))])
(fn [[x y]] (prn (+ x y))))
* Coercion operator and implicit coercion
kitchen-async provides a fn named p/->promise
, which coerces an arbitrary value to a Promise. By default, p/->promise
behaves as follows:
- For Promises, acts like
identity
(i.e. returns the argument as is) - For any other type of values, acts like
p/resolve
In fact, most functions defined as the thin wrapper API (and the macros that will be described below) implicitly apply p/->promise
to their input values. Thanks to that trick, you can freely mix up non-Promise values together with Promises:
(p/then 42 prn)
;; it will output 42 with no error
(p/then (p/all [21 (p/resolve 21)])
(fn [[x y]] (prn (+ x y))))
;; this also works well
Moreover, since it's defined as a protocol method, it's possible to extend p/->promise
to customize its behavior for a specific data type. For details, see the section "Extension of coercion operator". Also, the section "Integration with core.async channels" may help you grasp how we can utilize this capability.
Idiomatic Clojure style syntactic sugar
kitchen-async also provides variant of several macros (including special forms) in clojure.core
that return a Promise instead of returning the expression value.
p/do
p/do
conjoins the expressions of the body with p/then
ignoring the intermediate values. For example:
(p/do
(expr1)
(expr2)
(expr3))
is equivalent to:
(p/then (expr1)
(fn [_]
(p/then (expr2)
(fn [_] (expr3)))))
p/let
p/let
is almost the same as p/do
except that it names each intermediate value with the corresponding name. For example:
(p/let [v1 (expr1)
v2 (expr2)]
(expr3))
is equivalent to:
(p/then (expr1)
(fn [v1]
(p/then (expr2)
(fn [v2] (expr3)))))
Note that the body of the p/let
is implicitly wrapped with p/do
when it has multiple expressions in it. For example, when you write some code like:
(p/let [v1 (expr1)]
(expr2)
(expr3))
the call to expr3
will be deferred until (expr2)
is resolved. To avoid this behavior, you must wrap the body with do
explicitly:
(p/let [v1 (expr1)]
(do
(expr2)
(expr3)))
Threading macros
kitchen-async also has its own ->
, ->>
, some->
and some->>
. For example:
(p/-> (expr) f (g c))
is equivalent to:
(-> (expr)
(p/then (fn [x] (f x)))
(p/then (fn [y] (g y c))))
and
(p/some-> (expr) f (g c))
is equivalent to:
(-> (expr)
(p/then (fn [x] (some-> x f)))
(p/then (fn [y] (some-> y (g c))))
Loops
For loops, you can use p/loop
and p/recur
:
(defn timeout [ms v]
(p/promise [resolve]
(js/setTimeout #(resolve v) ms)))
(p/loop [i (timeout 1000 10)]
(when (> i 0)
(prn i)
(p/recur (timeout 1000 (dec i)))))
;; Count down the numbers from 10 to 1
Note that the body of the p/loop
is wrapped with p/do
, as in the p/let
.
p/recur
cannot be used outside of the p/loop
, and also make sure to call p/recur
at a tail position.
Error handling
For error handling, you can use p/try
, p/catch
and p/finally
:
(p/try
(expr)
(p/catch js/Error e
(js/console.error e))
(p/finally
(teardown)))
is almost equivalent to:
(-> (expr)
(p/catch*
(fn [e]
(if (instance? js/Error e)
(js/console.error e)
(throw e))))
(p/then (fn [v] (p/do (teardown) v))))
Note that the body of the p/try
, p/catch
and p/finally
is wrapped with p/do
, as in the p/let
.
p/catch
and p/finally
(if any) cannot be used outside of the p/try
, and also make sure to call them at the end of the p/try
's body.
Extension of coercion operator
(TODO)
Integration with core.async channels
(TODO)
License
Copyright © 2017 Shogo Ohta
Distributed under the Eclipse Public License 1.0.