Introduction
A lightweight promise/future abstraction built on top of JDK8 CompletableFuture
.
Project Maturity
Since promissum is a young project there may be some API breakage.
Install
The simplest way to use promissum library in a Clojure project is by including it as a dependency:
[funcool/promissum "0.3.3"]
User Guide
Introduction
The promise consists in a container that eventually will contain a value with builtin support for error handling. So the promise has three different states:
-
resolved
: means that the promise contains a value. -
rejected
: means thet the promise contains an error.
In summary: is the abstraction that represents the result of an asynchronous operation that will be eventually available.
The promissum's promise abstraction just works on top of the awesome
CompletableFuture
future/promise implementation available in JDK8. And offers
a very lightweight layer on top of it.
Note
|
Clojure comes with a builtin promise abstraction but it is designed only for blocking operations, and in async environments the blocking operations are completely discouraged. |
Creating a promise
It there several different ways to create a promise in promissum library. You can create it already resolved with initial value or already rejected with an exception.
Let start with a basic example using the commonly known promise delivering in clojure:
(require '[promissum.core :as p])
(def pr (p/promise))
(future
(Thread/sleep 200)
(p/deliver pr 20))]
(p/then pr (fn [v]
(println v)))
;; After 200ms it will print `20`
An other way to create a promise is using a factory function that can resolve or reject promise in asynchronous way. If you are familiar with javascript promises, you will found that very familiar:
(def pr (p/promise
(fn [deliver]
(deliver 1))))
promissum also exposes a clojure’s future
alternative that works in the same
way with the difference that it returns a CompletableFuture
:
(def pr (p/future
(Thread/sleep 200)
2))
@pr
;; => 2
You should know that promise
and future
functions just return
a CompletableFuture
instance without additional wrapping.
Creating a future
The promissum library also exposes a convenient macro for clojure future
macro
replacement. It works in exactly manner that the clojure version, with a little
difference that it returns a composable promise that can be easily chained in an
asynchronous way.
See an example:
(deref (p/future
(Thread/sleep 200)
(+ 1 2)))
;; => 3
Blocking operations
The promissum's promises can be used as drop in replacement for clojure promises, because them offers also blocking operations:
@pr
;; => 1
If you try to deref a promise that is rejected, the exception will be rereaised in
the calling thread. You should take care that the reraised exception is wrapped in
ExecutionException
in the same way as builtin clojure promise/future does.
For avouid unnecesary pain constantly handling that, promissum exposes the
await
function. It has the same call signature as clojure builtin deref function
but if promise contains a exception that exception will be reraised as is (without
additional wrapping).
(p/await pr)
;; => 1
State checking
promissum provides useful predicates that will allow check the state of a promise in any time.
Let see some examples:
(def pr (p/promise 2))
(p/promise? pr)
;; => true
(p/pending? pr)
;; => false
(p/resolved? pr)
;; => true
(p/rejected? pr)
;; => false
(p/done? pr)
;; => true
The done?
predicate checks if a promise is fullfiled, independently if is resolved
or rejected.
Promise chaining
It there different ways to compose/chain computations using promises. We will start with the basic: lineal way of chaining computations.
That can be done using then
or chain
functions exposed in promissum.core
namespace. Bot them are mainly interchangeable. The main differencia is that
chain
is variadic and then
not.
(def pr (-> (p/promise 2)
(p/then inc)
(p/then inc)))
(p/await pr)
;; => 4
And here the same example using the chain
function instead of then
:
(def pr (p/chain (p/promise 2) inc inc))
(p/await pr)
;; => 4
Later, thanks to the cats library, it there
other few methods of create promise compositions in more powerfull way: mlet
and alet
macros.
For demostration purposes, imagine that you have this function that emulates async operation and return a promise:
(require '[cats.core :as m])
(require '[promissum.core :as p])
(defn sleep-promise
[wait]
(p/promise (fn [deliver]
(Thread/sleep wait)
(deliver wait))))
Now, we will try to use this function together with mlet
macro and additionally
messure the execution time:
(time
(p/await (m/mlet [x (sleep-promise 42)
y (sleep-promise 41)]
(m/return (+ x y)))))
;; "Elapsed time: 84.328182 msecs"
;; => 83
The mlet
bindings are executed sequentially, waiting in each step for promise
resolution. If an error occurs in some step, the entire composition will be
short-circuited, returing exceptionally resolved promise.
The main disadvantage of mlet
is that it’s evaluation model is strictly
secuential. It is ok for some use cases, when the sequential order is mandatory.
But, if the strictly secuential model is not mandatory, mlet
does not take
the advantage of concurrency.
For solve this problem, it there alet
macro. It is almost identical to mlet
from the user experience, but internally it is based in very different abstractions.
Now, we will try to do the same example but using the alet
macro:
(time
@(m/alet [x (sleep-promise 42)
y (sleep-promise 41)]
(+ x y)))
;; "Elapsed time: 44.246427 msecs"
;; => 83
We can observe that the return value is identical to the previous example,
but it takes almost half of time to finish execute all the computations. This
is happens because alet
is more smarter macro and calculates de dependencies
between declared bindings and executes them in batches; taking fully advantage
of having fully miltithreaded/concurrent environment as is JVM.
You can read more about that here.
Error handling
One of the advantages of using promise abstraction is that it natively has a notion of error, so you don’t need reinvent it. If some of the computations of the composed promise chain/pipeline raises an exception, that one is automatically propagated to the last promise making the effect of short-circuiting.
Let see an example:
(def pr (p/chain (p/promise 2)
(fn [v] (throw (ex-info "test" {})))))
(p/await pr)
;; => clojure.lang.ExceptionInfo "test" ...
For exception catching facilities, promissum exposes a catch
function. It just
works like then
but with exceptions. It attaches a next computation that only
will be executend if a previous computation resolves exceptionally:
(def pr (-> (p/promise 2)
(p/then (fn [v] (throw (ex-info "foobar" {}))))
(p/catch (fn [error] :nothing))))
(p/await pr)
;; => :nothing
The catch
chain function also return a promise, that will be resolved or rejected
depending on that will happen inside the catch handler.
Working with collections
In some circumstances you will want wait a completion of few promises at same time, and promissum also provides helpers for that.
Imagine that you have a collection of promises and you want to wait until
all of them are resolved. This can be done using the all
combinator:
(def pr (p/all [(p/promise 1)
(p/promise 2)]))
(p/await pr)
;; => [1 2]
It there are also circumstances where you only want arbitrary select of the
first resolved promise. For this case, you can use the any
combinator:
(def pr (p/any [(p/promise 1)
(p/promise (ex-info "error" {}))]))
(p/await pr)
;; => 1
Later, for more advanced use cases, promissum is an algebraic structure that
implements the associative binary operation usually called mappend
:
(require '[cats.core :as m])
(def pr (m/mappend (p/promise {:a 1})
(p/promise {:b 2})))
(p/await pr)
;; => {:a 1 :b 2}
If you are interested in knowing more about it, plase refer to the cats documentation.
Developers Guide
Contribute
Unlike Clojure and other Clojure contrib libs, does not have many restrictions for contributions. Just open a issue or pull request.
Get the Code
promissum is open source and can be found on github.
You can clone the public repository with this command:
git clone https://github.com/funcool/promissum
License
promissum is licensed under BSD (2-Clause) license:
Copyright (c) 2015 Andrey Antukh <niwi@niwi.nz> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.