A Promise library for ClojureScript, or a poor man's core.async
async/await in ECMAScriptkitchen-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 macrop/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/rejectp/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/racep/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:
identity (i.e. returns the argument as is)p/resolveIn 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/dop/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/letp/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.
Can you improve this documentation? These fine people already did:
Shogo Ohta, OHTA Shogo & Alexis SchadEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |