A Promise library for ClojureScript, or a poor man's core.async
- 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.
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))))
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>}
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])
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
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
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
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))))
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.
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
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
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)))
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))))
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.
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.
(TODO)
(TODO)
Copyright © 2017 Shogo Ohta
Distributed under the Eclipse Public License 1.0.