[funcool/promesa "5.1.0"]
A promise library for Clojure and ClojureScript.
On the JVM paltform promesa is built on top of completable futures (requires jdk>=8). On JS engines it is built on top of the execution environment built-in Promise implementation.
Leiningen:
[funcool/promesa "5.1.0"]
deps.edn:
funcool/promesa {:mvn/version "5.1.0"}
This package requires JDK >= 8 if you are using it on the JVM. |
A promise is an abstraction that represents the result of an asynchronous operation that has the notion of error.
This is a list of all possible states for a promise:
resolved
: means that the promise contains a value.
rejected
: means that the promise contains an error.
pending
: means that the promise does not have value.
The promise can be considered done when it is resolved or rejected.
There are several different ways to create a promise instance. If you just want
to create a promise with a plain value, you can use the polymorphic promise
function:
(require '[promesa.core :as p])
;; creates a promise from value
(p/promise 1)
;; creates a rejected promise
(p/promise (ex-info "error" {}))
It automatically coerces the provided value to the appropriate promise instance:
rejected
when the provided value is an exception and resolved
in all other
cases.
If you already know that the value is either resolved
or rejected
, you can
skip the coercion and use the resolved
and rejected
functions:
;; Create a resolved promise
(p/resolved 1)
;; => #object[java.util.concurrent.CompletableFuture 0x3e133219 "resolved"]
;; Create a rejected promise
(p/rejected (ex-info "error" {}))
;; => #object[java.util.concurrent.CompletableFuture 0x3e563293 "rejected"]
Another option is to create an empty promise using the deferred
function
and provide the value asynchronously using p/resolve!
and p/reject!
:
(defn sleep
[ms]
(let [p (p/deferred)]
(future (p/resolve! p))
p))
Another option is using a factory function. If you are familiar with JavaScript, this is a similar approach.
Example creating a promise instance using a factory.
@(p/create (fn [resolve reject] (resolve 1)))
;; => 1
The factory will be executed synchronously (in the current thread) but if you want to execute it asynchronously, you can provide an executor:
(require '[promesa.exec :as exec])
@(p/create (fn [resolve reject] (resolve 1)) exec/default-executor)
;; => 1
Another way to create a promise is using the do!
macro:
(p/do!
(let [a (rand-int 10)
b (rand-int 10)]
(+ a b)))
The do!
macro works similarly to clojure’s do
block, so you can provide any
expression, but only the last one will be returned. That expression can be a
plain value or another promise.
If an exception is raised inside the do!
block, it will return the rejected
promise instead of re-raising the exception on the stack.
If the do!
contains more than one expression, each expression will be treated
as a promise expression and will be executed sequentially, each awaiting the
resolution of the prior expression.
For example, this do!
macro:
(p/do! (expr1)
(expr2)
(expr3))
Is roughtly equivalent to:
(p/let [_ (expr1)
_ (expr2)]
(expr3))
Finally, promesa exposes a future
macro very similar to the
clojure.core/future
:
@(p/future (some-complex-task))
;; => "result-of-complex-task"
One difference from clojure.core/future
is that if the return value of the
future expression is itself a promise instance, then it will await and unwrap
the inner promise:
@(p/future (p/future (p/future 1)))
;; => 1
The most common way to chain a transformation to a promise is using the general
purpose then
function:
@(-> (p/resolved 1)
(p/then inc))
;; => 2
;; flatten result
@(-> (p/resolved 1)
(p/then (fn [x] (p/resolved (inc x)))))
;; => 2
As you can observe in the example, it handles functions that return plain values as well as functions that return promise instances (which will automatically be flattened).
If you know that the chained function will always return plain values, you
can use the then' more performant variant of this function.
|
In the same line as the then'
function, there is a map
. It works identically to
it, the unique difference is the order of arguments:
(def result
(->> (p/resolved 1)
(p/map inc)))
@result
;; => 2
If you have multiple transformations and you want to apply them in one step,
there are the chain
and chain'
functions:
(def result
(-> (p/resolved 1)
(p/chain inc inc inc)))
@result
;; => 4
these are analogous to then and then' but accept multiple
transformation functions.
|
If you want to handle rejected and resolved callabacks in one unique callback,
then you can use the handler
chain function:
(def result
(-> (p/promise 1)
(p/handle (fn [result error]
(if error :rejected :resolved)))))
@result
;; => :resolved
And finally if you want to attach a (potentially side-effectful) callback to be
always executed notwithstanding if the promise is rejected or resolved, there is a
finally
function (very similar to try/finally):
(def result
(-> (p/promise 1)
(p/handle (fn []
(println "finally")))))
@result
;; => 1
;; => stdout: "finally"
let
The promesa library comes with convenient syntactic-sugar that allows you to
create a composition that looks like synchronous code while using the clojure’s
familiar let
syntax:
(require '[promesa.exec :as exec])
;; A function that emulates asynchronos behavior.
(defn sleep-promise
[wait]
(p/promise (fn [resolve reject]
(exec/schedule! wait #(resolve wait)))))
(def result
(p/let [x (sleep-promise 42)
y (sleep-promise 41)
z 2]
(+ x y z)))
@result
;; => 85
The let
macro behaves identically to the let
with the exception that it
always return a promise. If an error occurs at any step, the entire composition
will be short-circuited, returning exceptionally resolved promise.
Under the hood, the previous let
macro evalutes to something like this:
(p/then (sleep-promise 42)
(fn [x] (p/then (sleep-promise 41)
(fn [y] (p/then 2 (fn [z]
(p/promise (do (+ x y z)))))))))
all
In some circumstances you will want wait for completion of several promises at
the same time. To help with that, promesa also provides the all
helper.
(let [p (p/all [(do-some-io)
(do-some-other-io)])]
(p/then p (fn [[result1 result2]]
(do-something-with-results result1 result2))))
plet
The plet
macro combines syntax of let
with all
; and enables a simple
declaration of parallel operations followed by a body expression that will be
executed when all parallel operations have successfully resolved.
@(p/plet [a (p/delay 100 1)
b (p/delay 200 2)
c (p/delay 120 3)]
(+ na b c))
;; => result: 6
The plet
macro is just a syntactic sugar on top of all
. The previous example
can be written using all
in this manner:
(p/all [(p/delay 100 1)
(p/delay 200 2)
(p/delay 120 3)]
(fn [[a b c]] (+ a b c)))
any
There are also circumstances where you only want the first successfully resolved
promise. For this case, you can use the any
combinator:
(let [p (p/any [(p/delay 100 1)
(p/delay 200 2)
(p/delay 120 3)])]
(p/then p (fn [x]
(.log js/console "The first one finished: " x))))
race
The race
function method returns a promise that fulfills or rejects as soon as
one of the promises in an iterable fulfills or rejects, with the value or reason
from that promise:
@(p/race [(p/delay 100 1)
(p/delay 110 2)])
;; => 1
One of the advantages of using the promise abstraction is that it natively has a notion of errors, so you don’t need reinvent it. If some computation inside the composed promise chain/pipeline raises an exception, the pipeline short-circuits and propogates the exception to the last promise in the chain.
Let see an example:
(-> (p/rejected (ex-info "error" nil))
(p/catch (fn [error]
(.log js/console error))))
The catch
function adds a new handler to the promise chain that will be called
when any of the previous promises in the chain are rejected or an exception is
raised. The catch
function also returns a promise that will be resolved or
rejected depending on that will happen inside the catch handler.
If you prefer map
-like parameters order, the err
function (and error
alias) works in same way as catch
but has parameters ordered like map
:
(->> (p/rejected (ex-info "error" nil))
(p/error (fn [error]
(.log js/console error))))
On the JVM platform the reject value must be an instance of |
JavaScript, due its single-threaded nature, does not allow you to block or
sleep. But, with promises you can emulate that functionality using delay
like
so:
(-> (p/delay 1000 "foobar")
(p/then (fn [v]
(println "Received:" v))))
;; After 1 second it will print the message
;; to the console: "Received: foobar"
The promise library also offers the ability to add a timeout to async
operations thanks to the timeout
function:
(-> (some-async-task)
(p/timeout 200)
(p/then #(println "Task finished" %))
(p/catch #(println "Timeout" %)))
In this example, if the async task takes more that 200ms then the promise will
be rejected with a timeout error and then successfully captured with the catch
handler.
In addition to the promise abstraction, this library also comes with a lightweight abstraction for scheduling task to be executed at some time in future:
schedule
function.(require '[promesa.exec :as exec])
(exec/schedule! 1000 (fn []
(println "hello world")))
This example shows you how you can schedule a function call to be executed 1 second in the future. It works the same way for both plaforms (clj and cljs).
The tasks can be cancelled using its return value:
(def task (exec/schedule! 1000 #(do-stuff)))
(p/cancel! task)
This section is mainly affects the JVM. |
Lets take this example as a context:
@(-> (p/delay 100 1)
(p/then' inc)
(p/then' inc))
;; => 3
This will create a promise that will resolve to 1
in 100ms (in a separated
thread); then the first inc
will be executed (in the same thread) and then
another inc
is executed (in the same thread). In total only one thread is
involved.
This default execution model is usually preferrable because it don’t abuse the task scheduling and leverages function inlining on the JVM.
But it does have drawbacks: this approach will block the thread until all of the chained callbacks are executed. For small chains this is not a problem. However, if your chain has a lot of functions and requires a lot of computation time, this might cause unexpected latency. It may block other threads in the thread pool from doing other, maybe more important, tasks.
For such cases, promesa exposes an additional arity for provide a user-defined executor to control where the chained callbacks are executed:
(require '[promesa.exec :as exec])
@(-> (p/delay 100 1)
(p/then inc exec/default-executor)
(p/then inc exec/default-executor))
;; => 3
This will schedule a separated task for each chained callback, making the whole system more responsive because you are no longer executing big blocking functions; instead you are executing many small tasks.
The exec/default-executor
is a ForkJoinPool
instance that is highly
optimized for lots of small tasks.
The promesa is a lightweight abstraction built on top of native facilities
(CompletableFuture
in the jvm and js/Promise
on cljs).
Internaly we have heavy use of protocols in order to expose a polimorphic and
user friendly api, and this have a little overhead on top of raw usage of
CompletableFuture
or Promise
. This is the latest micro benchmark
(2019-09-17) that shows the real overhead of this library in contrat to use
plain native abstractions:
(run-bench (simple-promise-chain-5-raw))
;; => amd64 Linux 5.2.9-arch1-1-ARCH 4 cpu(s)
;; => OpenJDK 64-Bit Server VM 12.0.2+10
;; => Runtime arguments: -Dclojure.compiler.direct-linking=true
;; => Evaluation count : 687647820 in 60 samples of 11460797 calls.
;; => Execution time sample mean : 82.617649 ns
;; => Execution time mean : 82.606811 ns
;; => Execution time sample std-deviation : 2.348589 ns
;; => Execution time std-deviation : 2.365164 ns
;; => Execution time lower quantile : 78.787962 ns ( 2.5%)
;; => Execution time upper quantile : 86.941501 ns (97.5%)
;; => Overhead used : 9.967315 ns
;; =>
(run-bench (simple-completable-chain-5-raw))
;; => amd64 Linux 5.2.9-arch1-1-ARCH 4 cpu(s)
;; => OpenJDK 64-Bit Server VM 12.0.2+10
;; => Runtime arguments: -Dclojure.compiler.direct-linking=true
;; => Evaluation count : 823532160 in 60 samples of 13725536 calls.
;; => Execution time sample mean : 62.267034 ns
;; => Execution time mean : 62.279349 ns
;; => Execution time sample std-deviation : 1.967931 ns
;; => Execution time std-deviation : 2.014908 ns
;; => Execution time lower quantile : 59.663843 ns ( 2.5%)
;; => Execution time upper quantile : 67.599822 ns (97.5%)
;; => Overhead used : 9.967315 ns
The benchmarked functions are:
(defn simple-promise-chain-5-raw
[]
@(as-> (CompletableFuture/completedFuture 1) $
(p/then' $ inc)
(p/then' $ inc)
(p/then' $ inc)
(p/then' $ inc)
(p/then' $ inc)))
(defn simple-completable-chain-5-raw
[]
@(as-> (CompletableFuture/completedFuture 1) $
(.thenApply ^CompletionStage $ ^Function (pu/->FunctionWrapper inc))
(.thenApply ^CompletionStage $ ^Function (pu/->FunctionWrapper inc))
(.thenApply ^CompletionStage $ ^Function (pu/->FunctionWrapper inc))
(.thenApply ^CompletionStage $ ^Function (pu/->FunctionWrapper inc))
(.thenApply ^CompletionStage $ ^Function (pu/->FunctionWrapper inc))))
Unlike Clojure and other Clojure contrib libs, this project does not have many restrictions for contributions. Just open a issue or pull request.
promesa is open source and can be found on github.
You can clone the public repository with this command:
git clone https://github.com/funcool/promesa
To run the tests execute the following:
For the JVM platform:
lein test
And for JS platform:
./scripts/build
node out/tests.js
You will need to have nodejs installed on your system.
promesa is licensed under BSD (2-Clause) license:
Copyright (c) 2015-2019 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.
Can you improve this documentation? These fine people already did:
Andrey Antukh, Alejandro Gómez, JarrodCTaylor, John Christopher Jones, Raphael Martin Schindler, Fernando Hurtado, Lauri Oherd, Lars Trieloff, Ricardo J. Mendez & Igor BondarenkoEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close