Giter Club home page Giter Club logo

kitchen-async's Introduction

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.

kitchen-async's People

Contributors

athos avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kitchen-async's Issues

Expansion result of threading macros is broken with JS interop

The following form should be completely valid:

(p/-> (js/fetch url) .json)

But, the expansion result of that is not:

=> (macroexpand '(p/-> (js/fetch url) .json))
(try
 (kitchen-async.promise/->
  (kitchen-async.promise/then (js/fetch url) .json))  ;; <- .json should be (fn [x#] (.json x#)) here
 (catch
  :default
  e__51305__auto__ 
  (kitchen-async.promise/reject e__51305__auto__)))

0.1.0 release

Could you consider releasing 0.1.0 so we don't have to depend on a SNAPSHOT dependency.

`core.async` interop?

Getting excited to use this library, but was hoping to find some docs on the interop syntax for js/Promise -> core.async. I noticed the library is proposing to do this. Any thoughts of when?

Keyword-get in threading macro does not work

First off, thank you for this library - I am finding it to be very useful!

I think I've found a bug with using plain keywords in the -> threading macro.

Using a plain keyword, the result is passed through unchanged:

(p/-> (new js/Promise (fn [resolve reject]
                        (resolve {:x 10})))
      :x
      (print))

=>  {:x 10}
    ^ incorrect

But if we use (get _ :kw), the correct result is returned:

(p/-> (new js/Promise (fn [resolve reject]
                        (resolve {:x 10})))
      (get :x)
      (print))

=> 10
   ^ correct

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.