A ClojureScript library for working with JavaScript promises.
This library leverages the power of core.async to let you write promise code like this:
(cljs-promises.async/extend-promises-as-pair-channels!)
(go
(let [user-promise (get-user "jamesmacaulay")]
(try
(println (:blog (<? user-promise)))
(catch js/Error e
(println (str "Could't get user data: " (ex-message e))))))
The first line globally extends Promise
instances to act like read-only channels which, once resolved, endlessly produce the same value or error to anyone who takes from them. Because promises have built-in error semantics which don't have any direct corollary in core.async, there are different ways that we can represent promise results. In this example we've extended promises in a way that values taken from them with core.async are actually [value error]
pairs, where you get [value nil]
from a fulfilled promise and [nil error]
from a rejected promise.
<?
is a macro provided by cljs-promises.async
which takes one of these pairs from a promise with core.async's <!
. If the error
slot of the pair is non-nil, it throws the error. Otherwise it returns whatever is in the value
slot.
If you want to customize how a promise's resolution translates to values you get from <!
, you can use the more general extend-promises-as-channels!
which lets you provide your own transform functions for fulfilled values and rejected errors.
If you don't want to globally extend promises like this at all (a good idea if you're writing a library or for some other reason don't want to mess with the global scope), the cljs-promises.async
namespace also provides functions which wrap promises on an ad-hoc basis:
cljs-promises.async/pair-port
takes a promise and gives you an object which acts like a read-only channel of[value error]
pairs as described above.cljs-promises.async/value-port
gives you aReadPort
of only resolved values, ornil
values if a promise is rejected.cljs-promises.async/error-port
is the reverse ofvalue-port
and gives you just the errors.
Some functions in this library require that you've already got an ES6 Promise implementation present, either natively or through a polyfill. Other functions only require that you're giving them Promises/A+-compliant promise objects.
We have CSP with core.async, so why bother with promises? Promises provide very different concurrency semantics compared with CSP channels. While channels provide a powerful abstraction for passing messages and synchronizing execution, a promise simply represents a single "eventual value".
This means that HTTP and most filesytem operations are very well suited for promises. Meanwhile, promises are not appropriate for coordinating streams of spontaneous events like key presses or data coming in on a WebSocket. Channels are still the bees' knees when it comes to that kind of thing.
Promises excel at representing single values because they are an immutable reference type:
- after a promise is created, the ability to resolve it to a value is not part of its public interface
- a promise only ever resolves to a single fulfilled value or rejected error
- once resolved, a promise will continue to provide its value or error to anyone who asks for it
(Sounds a lot like futures in Clojure, doesn't it?)
Channels in core.async are very different: they are inherently mutable. When you take a value from a channel, it is gone from that channel and no one else will see it. Here's how you might do a JSONP request in core.async:
;; jsonp function lifted from http://swannodette.github.io/2013/11/07/clojurescript-101/
(defn jsonp [uri]
(let [out (chan)
req (Jsonp. (Uri. uri))]
(.send req nil (fn [res] (put! out res)))
out))
(let [uri "http://en.wikipedia.org/w/api.php?action=opensearch&format=json&search=clojure"]
(go (println (<! (jsonp uri)))))
This is fine, but it means that you have to treat the return value of jsonp
as a single-use wrapper which becomes useless after its value has been taken from it.
If you want to be able to share representations of delayed or partially-delayed values (e.g. nested data structures with some values yet to arrive), then promises are a better fit. Because they are immutable, you can share them between different parts of your code and not have to worry about how they'll be used.
Here's some code which builds a map of promises for different kinds of data about a GitHub user:
(defn build-view-context
[username]
{:user (github-get (str "/users/" username))
:gists (github-get (str "/users/" username "/gists"))
:repos (github-get (str "/users/" username "/repos?sort=created"))
:events (github-get (str "/users/" username "/events"))})
This map can be shared among multiple view functions, and each of those functions can depend on any subset of the included promises. The code which builds a context of eventual values doesn't need to know how those values are going to be used.
I'm sure there are various possibilities available with core.async channels to decouple things in similar ways. There is mult, and pub/sub, and probably other tools that would help. I would argue, however, that the simplicity of promises makes them a better tool for this particular job.
One of the great things about core.async, though, is that its channels are based on a simple interface composed of a handful of very granular ClojureScript protocols! This means that it's actually really easy to make custom types which act like channels just enough to play nice with the rest of core.async.
I've been developing cljs-promises with Light Table, mostly by playing with the examples:
- run
lein cljsbuild auto examples
in the project root to watch the filesystem and compile-on-save - add a Light Table connection to an external browser and copy the port number from the script tag
- edit
examples/index.html
so that the script tag matches the port - open up
examples/index.html
in a browser
Then you can eval individual forms from Light Table and save-and-refresh whenever you make big changes.
Special thanks to David Nolen for writing so much fantastic code and introductory material for newcomers to ClojureScript and especially core.async. I've adapted or copied some code from his blog in this library (especially the examples), and I've tried to make attribution notes in the code comments wherever appropriate.
This code is released under an MIT license.
Copyright 2014 James MacAulay.