Introduction

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.

Project Maturity

Since promesa is a young project there may be some API breakage.

Install

Just include the following lines in your dependency vector on project.clj:

[funcool/promesa "4.0.0"]

This package requires JDK >= 8 if you are using it on the JVM.

User guide

Introduction

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.

Creating a promise

There are several different ways to create a promise instance. If you just want to create a promise with a value, you can use the promise function:

;; creates a promise from value
(p/promise 1)

;; creates a rejected promise
(p/promise (ex-info "error" {}))

The promise consturctor function automatcially coerce the value to the approprite promise, returning rejected promises for exceptions and resolved promises for the rest of values.

If you already known the value and want to create resolved or rejected promise instances, yo can use resolved and rejected functions:

(require '[promesa.core :as p])

;; Create a resolved promise
(p/resoved 1)
;; => #<Promise [~]>

;; Create a rejected promise
(p/rejected (ex-info "error" {}))
;; => #<Promise [~]>

They are more performant because they does not perform any polimorphic coercion on the value. They create the promise with the provided value as-is.

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)]
    (js/setTimeout #(p/resolve! p))
    p))

Another option is using a factory function. If you are familiar with JavaScript, there 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 to use the do! macro. The do! macro works similarly to the factory callback, except that we can return a value directly instead of calling resolve:

(p/do!
  (let [a (rand-int 10)
        b (rand-int 10)]
    (+ a b)))

The do! expression work 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 an other 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

Promise Chaining

The most common way to apply a function to a promise (or in other words, to chain its execution) is using the well-known map function:

(def result
  (->> (p/resolved 1)
       (p/map inc)))

@result
;; => 2

For people coming from the JS world, there is also the then function that works in very similar way to map with the exception that the parameters have different order and flatten automatically.

@(-> (p/resolved 1)
     (p/then inc))
;; => 2

;; flatten result
@(-> (p/resolved 1)
     (p/then (fn [x] (p/resolved (inc x)))))
;; => 2

If you want to apply multiple functions instead of using multiple then or map`s, you can use the `chain function:

(def result
  (-> (p/resolved 1)
      (p/chain inc inc inc)))

@result
;; => 4

The chain function uses then behind the scenes, so the same rules about flattening applies.

Note
if you know that your chained functions do not return promises, you can use chain' instead for better performance.

Promise Composition

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])

(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/resolved (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 helpers.

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:

(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. It 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

Error handling

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))))
Note

On the JVM platform the reject value must be an instance of Throwable, but on the JavaScript platform the reject value can be any value.

Delays and timeouts.

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.

Scheduling Tasks

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:

Example using the 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)

Advanced topics

Execution model

Note
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 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.

In some cases you may still want execute all chained functions together but all together in different executor. In this case you can use the deferred constructor to specify the executor.

@(-> (p/delay 100 1)
     (p/deferred exec/default-executor)
     (p/chain' inc inc))

Performance overhead

The promesa is a lightweight abstraction built on top of native abstractions (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, but 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))))

Developers Guide

Contributing

Unlike Clojure and other Clojure contrib libs, this project does not have many restrictions for contributions. Just open a issue or pull request.

Get the Code

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

Run tests

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.

License

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.