Liking cljdoc? Tell your friends :D

promesa - promise library for clojure and clojurescript

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.

Install

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.

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

Promise Chaining

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"

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

;; 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

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

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

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.

Performance overhead

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

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.

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 Bondarenko
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close