Skip to content

A Promise library for ClojureScript, or a poor man's core.async

License

Notifications You must be signed in to change notification settings

athos/kitchen-async

Repository files navigation

kitchen-async

Clojars Project CircleCI

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:

Clojars Project

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.

About

A Promise library for ClojureScript, or a poor man's core.async

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published