├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc ├── Makefile ├── content-docinfo.html └── content.adoc ├── project.clj ├── src └── promissum │ ├── core.clj │ └── protocols.clj └── test └── promissum └── core_tests.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | /*-init.clj 11 | /doc/dist/ 12 | /output 13 | /repl 14 | /node_modules 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## Version 0.3.3 ## 4 | 5 | Date: 2015-11-04 6 | 7 | - Add branch function. 8 | 9 | 10 | ## Version 0.3.2 ## 11 | 12 | Date: 2015-10-17 13 | 14 | - Improved stacktraces on exception is rerasied from `await` function. 15 | 16 | 17 | ## Version 0.3.1 ## 18 | 19 | Date: 2015-09-19 20 | 21 | - Regression introduced in 0.3.0 related to cats 1.0.0 compatibility. 22 | 23 | 24 | ## Version 0.3.0 ## 25 | 26 | Date: 2015-09-18 27 | 28 | - Update documentation. 29 | - Adapt to the cats 1.0.0 release. 30 | - Now implements semigroup abstraction. 31 | - Change own protocol names to use "-" prefix. 32 | 33 | WARNING: the uploaded version is broken because 34 | it misses some additional changes, that are now 35 | available in 0.3.1. 36 | 37 | 38 | ## Version 0.2.0 ## 39 | 40 | Date: 2015-08-11 41 | 42 | - Update cats dependency to 0.6.1 43 | 44 | 45 | ## Version 0.1.0 ## 46 | 47 | Date: 2015-07-21 48 | 49 | - First relase. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Andrey Antukh 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # promissum # 2 | 3 | A composable promise/future library for Clojure (built on top of jdk8 completable future). 4 | 5 | This package targets Clojure 1.7 and JDK8. 6 | 7 | [![Clojars Project](http://clojars.org/funcool/promissum/latest-version.svg)](http://clojars.org/funcool/promissum) 8 | 9 | See the [documentation](https://funcool.github.io/promissum/latest/) or 10 | [api reference](https://funcool.github.io/promissum/latest/api/) for more detailed 11 | information. 12 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | all: doc 2 | 3 | api: 4 | cd .. 5 | lein doc 6 | 7 | doc: 8 | mkdir -p dist/latest/ 9 | asciidoctor -a docinfo -a stylesheet! -o dist/latest/index.html content.adoc 10 | 11 | github: api doc 12 | ghp-import -m "Generate documentation" -b gh-pages dist/ 13 | git push origin gh-pages 14 | -------------------------------------------------------------------------------- /doc/content-docinfo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /doc/content.adoc: -------------------------------------------------------------------------------- 1 | = promissum - composable promise/future library for Clojure 2 | Andrey Antukh, 3 | 0.3.3 4 | :toc: left 5 | :toclevels: 2 6 | :!numbered: 7 | :idseparator: - 8 | :idprefix: 9 | :sectlinks: 10 | :source-highlighter: pygments 11 | :pygments-style: friendly 12 | 13 | 14 | == Introduction 15 | 16 | A lightweight promise/future abstraction built on top of JDK8 `CompletableFuture`. 17 | 18 | 19 | === Project Maturity 20 | 21 | Since _promissum_ is a young project there may be some API breakage. 22 | 23 | 24 | === Install 25 | 26 | The simplest way to use _promissum_ library in a Clojure project is by including 27 | it as a dependency: 28 | 29 | [source, clojure] 30 | ---- 31 | [funcool/promissum "0.3.3"] 32 | ---- 33 | 34 | 35 | == User Guide 36 | 37 | === Introduction 38 | 39 | The promise consists in a container that eventually will contain a value with 40 | builtin support for error handling. So the promise has three different states: 41 | 42 | - `resolved`: means that the promise contains a value. 43 | - `rejected`: means thet the promise contains an error. 44 | 45 | In summary: is the abstraction that represents the result of an asynchronous 46 | operation that will be eventually available. 47 | 48 | The _promissum_'s promise abstraction just works on top of the awesome 49 | `CompletableFuture` future/promise implementation available in JDK8. And offers 50 | a very lightweight layer on top of it. 51 | 52 | NOTE: Clojure comes with a builtin promise abstraction but it is designed only for 53 | blocking operations, and in async environments the blocking operations are 54 | completely discouraged. 55 | 56 | 57 | === Creating a promise 58 | 59 | It there several different ways to create a promise in _promissum_ library. You can 60 | create it already resolved with initial value or already rejected with an exception. 61 | 62 | Let start with a basic example using the commonly known promise delivering in 63 | clojure: 64 | 65 | [source, clojure] 66 | ---- 67 | (require '[promissum.core :as p]) 68 | 69 | (def pr (p/promise)) 70 | 71 | (future 72 | (Thread/sleep 200) 73 | (p/deliver pr 20))] 74 | 75 | (p/then pr (fn [v] 76 | (println v))) 77 | 78 | ;; After 200ms it will print `20` 79 | ---- 80 | 81 | An other way to create a promise is using a factory function that can resolve or 82 | reject promise in asynchronous way. If you are familiar with javascript promises, 83 | you will found that very familiar: 84 | 85 | [source, clojure] 86 | ---- 87 | (def pr (p/promise 88 | (fn [deliver] 89 | (deliver 1)))) 90 | ---- 91 | 92 | _promissum_ also exposes a clojure's `future` alternative that works in the same 93 | way with the difference that it returns a `CompletableFuture`: 94 | 95 | [source, clojure] 96 | ---- 97 | (def pr (p/future 98 | (Thread/sleep 200) 99 | 2)) 100 | @pr 101 | ;; => 2 102 | ---- 103 | 104 | You should know that `promise` and `future` functions just return 105 | a `CompletableFuture` instance without additional wrapping. 106 | 107 | 108 | === Creating a future 109 | 110 | The _promissum_ library also exposes a convenient macro for clojure `future` macro 111 | replacement. It works in exactly manner that the clojure version, with a little 112 | difference that it returns a composable promise that can be easily chained in an 113 | asynchronous way. 114 | 115 | See an example: 116 | 117 | [source, clojure] 118 | ---- 119 | (deref (p/future 120 | (Thread/sleep 200) 121 | (+ 1 2))) 122 | ;; => 3 123 | ---- 124 | 125 | 126 | === Blocking operations 127 | 128 | The _promissum_'s promises can be used as drop in replacement for clojure promises, 129 | because them offers also blocking operations: 130 | 131 | [source, clojure] 132 | ---- 133 | @pr 134 | ;; => 1 135 | ---- 136 | 137 | If you try to deref a promise that is rejected, the exception will be rereaised in 138 | the calling thread. You should take care that the reraised exception is wrapped in 139 | `ExecutionException` in the same way as builtin clojure promise/future does. 140 | 141 | For avouid unnecesary pain constantly handling that, _promissum_ exposes the 142 | `await` function. It has the same call signature as clojure builtin deref function 143 | but if promise contains a exception that exception will be reraised as is (without 144 | additional wrapping). 145 | 146 | [source, clojure] 147 | ---- 148 | (p/await pr) 149 | ;; => 1 150 | ---- 151 | 152 | 153 | === State checking 154 | 155 | _promissum_ provides useful predicates that will allow check the state of a promise 156 | in any time. 157 | 158 | Let see some examples: 159 | 160 | [source, clojure] 161 | ---- 162 | (def pr (p/promise 2)) 163 | 164 | (p/promise? pr) 165 | ;; => true 166 | 167 | (p/pending? pr) 168 | ;; => false 169 | 170 | (p/resolved? pr) 171 | ;; => true 172 | 173 | (p/rejected? pr) 174 | ;; => false 175 | 176 | (p/done? pr) 177 | ;; => true 178 | ---- 179 | 180 | The `done?` predicate checks if a promise is fullfiled, independently if is resolved 181 | or rejected. 182 | 183 | 184 | === Promise chaining 185 | 186 | It there different ways to compose/chain computations using promises. We will start 187 | with the basic: lineal way of chaining computations. 188 | 189 | That can be done using `then` or `chain` functions exposed in `promissum.core` 190 | namespace. Bot them are mainly interchangeable. The main differencia is that 191 | `chain` is variadic and `then` not. 192 | 193 | [source, clojure] 194 | ---- 195 | (def pr (-> (p/promise 2) 196 | (p/then inc) 197 | (p/then inc))) 198 | 199 | (p/await pr) 200 | ;; => 4 201 | ---- 202 | 203 | And here the same example using the `chain` function instead of `then`: 204 | 205 | [source, clojure] 206 | ---- 207 | (def pr (p/chain (p/promise 2) inc inc)) 208 | (p/await pr) 209 | ;; => 4 210 | ---- 211 | 212 | Later, thanks to the link:https://github.com/funcool/cats[cats] library, it there 213 | other few methods of create promise compositions in more powerfull way: `mlet` 214 | and `alet` macros. 215 | 216 | For demostration purposes, imagine that you have this function that emulates async 217 | operation and return a promise: 218 | 219 | [source, clojure] 220 | ---- 221 | (require '[cats.core :as m]) 222 | (require '[promissum.core :as p]) 223 | 224 | (defn sleep-promise 225 | [wait] 226 | (p/promise (fn [deliver] 227 | (Thread/sleep wait) 228 | (deliver wait)))) 229 | ---- 230 | 231 | Now, we will try to use this function together with `mlet` macro and additionally 232 | messure the execution time: 233 | 234 | [source, clojure] 235 | ---- 236 | (time 237 | (p/await (m/mlet [x (sleep-promise 42) 238 | y (sleep-promise 41)] 239 | (m/return (+ x y))))) 240 | ;; "Elapsed time: 84.328182 msecs" 241 | ;; => 83 242 | ---- 243 | 244 | The `mlet` bindings are executed sequentially, waiting in each step for promise 245 | resolution. If an error occurs in some step, the entire composition will be 246 | short-circuited, returing exceptionally resolved promise. 247 | 248 | The main disadvantage of `mlet` is that it's evaluation model is strictly 249 | secuential. It is ok for some use cases, when the sequential order is mandatory. 250 | But, if the strictly secuential model is not mandatory, `mlet` does not take 251 | the advantage of concurrency. 252 | 253 | For solve this problem, it there `alet` macro. It is almost identical to `mlet` 254 | from the user experience, but internally it is based in very different abstractions. 255 | 256 | Now, we will try to do the same example but using the `alet` macro: 257 | 258 | [source, clojure] 259 | ---- 260 | (time 261 | @(m/alet [x (sleep-promise 42) 262 | y (sleep-promise 41)] 263 | (+ x y))) 264 | ;; "Elapsed time: 44.246427 msecs" 265 | ;; => 83 266 | ---- 267 | 268 | We can observe that the return value is identical to the previous example, 269 | but it takes almost half of time to finish execute all the computations. This 270 | is happens because `alet` is more smarter macro and calculates de dependencies 271 | between declared bindings and executes them in batches; taking fully advantage 272 | of having fully miltithreaded/concurrent environment as is JVM. 273 | 274 | You can read more about that link:http://funcool.github.io/cats/latest/#syntax-sugar[here]. 275 | 276 | 277 | === Error handling 278 | 279 | One of the advantages of using promise abstraction is that it natively has 280 | a notion of error, so you don't need reinvent it. If some of the computations 281 | of the composed promise chain/pipeline raises an exception, that one is 282 | automatically propagated to the last promise making the effect of short-circuiting. 283 | 284 | Let see an example: 285 | 286 | [source, clojure] 287 | ---- 288 | (def pr (p/chain (p/promise 2) 289 | (fn [v] (throw (ex-info "test" {}))))) 290 | (p/await pr) 291 | ;; => clojure.lang.ExceptionInfo "test" ... 292 | ---- 293 | 294 | For exception catching facilities, _promissum_ exposes a `catch` function. It just 295 | works like `then` but with exceptions. It attaches a next computation that only 296 | will be executend if a previous computation resolves exceptionally: 297 | 298 | [source, clojure] 299 | ---- 300 | (def pr (-> (p/promise 2) 301 | (p/then (fn [v] (throw (ex-info "foobar" {})))) 302 | (p/catch (fn [error] :nothing)))) 303 | 304 | (p/await pr) 305 | ;; => :nothing 306 | ---- 307 | 308 | The `catch` chain function also return a promise, that will be resolved or rejected 309 | depending on that will happen inside the catch handler. 310 | 311 | 312 | === Working with collections 313 | 314 | In some circumstances you will want wait a completion of few promises at same time, 315 | and _promissum_ also provides helpers for that. 316 | 317 | Imagine that you have a collection of promises and you want to wait until 318 | all of them are resolved. This can be done using the `all` combinator: 319 | 320 | [source, clojure] 321 | ---- 322 | (def pr (p/all [(p/promise 1) 323 | (p/promise 2)])) 324 | (p/await pr) 325 | ;; => [1 2] 326 | ---- 327 | 328 | It there are also circumstances where you only want arbitrary select of the 329 | first resolved promise. For this case, you can use the `any` combinator: 330 | 331 | [source, clojure] 332 | ---- 333 | (def pr (p/any [(p/promise 1) 334 | (p/promise (ex-info "error" {}))])) 335 | (p/await pr) 336 | ;; => 1 337 | ---- 338 | 339 | Later, for more advanced use cases, _promissum_ is an algebraic structure that 340 | implements the associative binary operation usually called `mappend`: 341 | 342 | [source, clojure] 343 | ---- 344 | (require '[cats.core :as m]) 345 | 346 | (def pr (m/mappend (p/promise {:a 1}) 347 | (p/promise {:b 2}))) 348 | (p/await pr) 349 | ;; => {:a 1 :b 2} 350 | ---- 351 | 352 | If you are interested in knowing more about it, plase refer to the 353 | link:https://github.com/funcool/cats[cats documentation]. 354 | 355 | 356 | == Developers Guide 357 | 358 | === Contribute 359 | 360 | Unlike Clojure and other Clojure contrib libs, does not have many restrictions for 361 | contributions. Just open a issue or pull request. 362 | 363 | 364 | == Get the Code 365 | 366 | _promissum_ is open source and can be found on 367 | link:https://github.com/funcool/promissum[github]. 368 | 369 | You can clone the public repository with this command: 370 | 371 | [source,text] 372 | ---- 373 | git clone https://github.com/funcool/promissum 374 | ---- 375 | 376 | 377 | === Run tests 378 | 379 | For run tests just execute this: 380 | 381 | [source, text] 382 | ---- 383 | lein test 384 | ---- 385 | 386 | 387 | === License 388 | 389 | _promissum_ is licensed under BSD (2-Clause) license: 390 | 391 | ---- 392 | Copyright (c) 2015 Andrey Antukh 393 | 394 | All rights reserved. 395 | 396 | Redistribution and use in source and binary forms, with or without 397 | modification, are permitted provided that the following conditions are met: 398 | 399 | * Redistributions of source code must retain the above copyright notice, this 400 | list of conditions and the following disclaimer. 401 | 402 | * Redistributions in binary form must reproduce the above copyright notice, 403 | this list of conditions and the following disclaimer in the documentation 404 | and/or other materials provided with the distribution. 405 | 406 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 407 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 408 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 409 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 410 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 411 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 412 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 413 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 414 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 415 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 416 | ---- 417 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject funcool/promissum "0.3.3" 2 | :description "A composable promise/future library for Clojure." 3 | :url "https://github.com/funcool/promissum" 4 | :license {:name "BSD (2 Clause)" 5 | :url "http://opensource.org/licenses/BSD-2-Clause"} 6 | :dependencies [[org.clojure/clojure "1.7.0" :scope "provided"] 7 | [funcool/cats "1.0.0"]] 8 | :deploy-repositories {"releases" :clojars 9 | "snapshots" :clojars} 10 | :source-paths ["src"] 11 | :test-paths ["test"] 12 | :jar-exclusions [#"\.swp|\.swo|user.clj"] 13 | :javac-options ["-target" "1.8" "-source" "1.8" "-Xlint:-options"] 14 | :profiles {:dev {:codeina {:sources ["src"] 15 | :reader :clojure 16 | :target "doc/dist/latest/api" 17 | :src-uri "http://github.com/funcool/cats/blob/master/" 18 | :src-uri-prefix "#L"} 19 | :plugins [[funcool/codeina "0.3.0"] 20 | [lein-ancient "0.6.7" 21 | :exclusions [org.clojure/tools.reader]]]}}) 22 | -------------------------------------------------------------------------------- /src/promissum/core.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Andrey Antukh 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer. 10 | ;; 2. Redistributions in binary form must reproduce the above copyright 11 | ;; notice, this list of conditions and the following disclaimer in the 12 | ;; documentation and/or other materials provided with the distribution. 13 | ;; 14 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | ;; IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | (ns promissum.core 26 | "A promise implementation for Clojure that uses jdk8 27 | completable futures behind the scenes." 28 | (:refer-clojure :exclude [future promise deliver await]) 29 | (:require [cats.core :as m] 30 | [cats.context :as mc] 31 | [cats.protocols :as mp] 32 | [promissum.protocols :as p]) 33 | (:import java.util.concurrent.CompletableFuture 34 | java.util.concurrent.CompletionStage 35 | java.util.concurrent.TimeoutException 36 | java.util.concurrent.ExecutionException 37 | java.util.concurrent.CompletionException 38 | java.util.concurrent.TimeUnit 39 | java.util.concurrent.Future 40 | java.util.concurrent.Executor 41 | java.util.concurrent.ForkJoinPool 42 | java.util.function.Function 43 | java.util.function.Supplier)) 44 | 45 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 46 | ;; Concurrency 47 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 48 | 49 | (def ^{:doc "The main executor service for schedule promises." 50 | :dynamic true} 51 | *executor* (ForkJoinPool/commonPool)) 52 | 53 | (defn function 54 | "Given an plain function `f`, return an 55 | instace of the java.util.concurrent.Function 56 | class." 57 | {:no-doc true} 58 | [f] 59 | (reify Function 60 | (apply [_ v] (f v)))) 61 | 62 | (defn schedule 63 | "Schedule a functon to execute in 64 | a provided executor service." 65 | {:no-doc true} 66 | ([func] 67 | (schedule *executor* func)) 68 | ([^Executor executor ^Runnable func] 69 | (.execute executor func))) 70 | 71 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 72 | ;; Implementation 73 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 74 | 75 | (declare promise-context) 76 | 77 | (defn- impl-get-context 78 | [^CompletionStage cs] 79 | promise-context) 80 | 81 | (defn- impl-extract 82 | [^CompletionStage cs] 83 | (try 84 | (.getNow cs nil) 85 | (catch ExecutionException e 86 | (.getCause e)) 87 | (catch CompletionException e 88 | (.getCause e)))) 89 | 90 | (defn- impl-rejected? 91 | [^CompletionStage cs] 92 | (.isCompletedExceptionally cs)) 93 | 94 | (defn- impl-resolved? 95 | [^CompletionStage cs] 96 | (and (not (.isCompletedExceptionally cs)) 97 | (not (.isCancelled cs)) 98 | (.isDone cs))) 99 | 100 | (defn- impl-done? 101 | [^CompletionStage cs] 102 | (.isDone cs)) 103 | 104 | (defn- impl-map 105 | [^CompletionStage cf cb] 106 | (.thenApplyAsync cf (function cb) *executor*)) 107 | 108 | (defn- impl-bind 109 | [^CompletionStage cf cb] 110 | (.thenComposeAsync cf (function cb) *executor*)) 111 | 112 | (defn- impl-catch 113 | [^CompletionStage cs callback] 114 | (->> (function #(callback (.getCause %))) 115 | (.exceptionally cs))) 116 | 117 | (defn- impl-deliver 118 | [^CompletableFuture cs v] 119 | (if (instance? Throwable v) 120 | (.completeExceptionally cs v) 121 | (.complete cs v))) 122 | 123 | (defn- impl-deref 124 | [cs] 125 | (try 126 | (.get cs) 127 | (catch ExecutionException e 128 | (let [e' (.getCause e)] 129 | (.setStackTrace e' (.getStackTrace e)) 130 | (throw e'))) 131 | (catch CompletionException e 132 | (let [e' (.getCause e)] 133 | (.setStackTrace e' (.getStackTrace e)) 134 | (throw e'))))) 135 | 136 | (defn- impl-await 137 | ([^Future cs] 138 | (try 139 | (.get cs) 140 | (catch ExecutionException e 141 | (let [e' (.getCause e)] 142 | (throw e'))) 143 | (catch CompletionException e 144 | (let [e' (.getCause e)] 145 | (throw e'))))) 146 | ([^Future cs ^long ms] 147 | (impl-await cs ms nil)) 148 | ([^Future cs ^long ms default] 149 | (try 150 | (.get cs ms TimeUnit/SECONDS) 151 | (catch TimeoutException e 152 | default) 153 | (catch ExecutionException e 154 | (let [e' (.getCause e)] 155 | (.setStackTrace e' (.getStackTrace e)) 156 | (throw e'))) 157 | (catch CompletionException e 158 | (let [e' (.getCause e)] 159 | (.setStackTrace e' (.getStackTrace e)) 160 | (throw e')))))) 161 | 162 | (extend CompletionStage 163 | mp/Contextual 164 | {:-get-context impl-get-context} 165 | 166 | mp/Extract 167 | {:-extract impl-extract} 168 | 169 | p/IState 170 | {:-rejected? impl-rejected? 171 | :-resolved? impl-resolved? 172 | :-done? impl-done?} 173 | 174 | p/IFuture 175 | {:-map impl-map 176 | :-bind impl-bind 177 | :-catch impl-catch}) 178 | 179 | (extend Future 180 | p/IAwaitable 181 | {:-await impl-await}) 182 | 183 | (extend CompletableFuture 184 | p/IPromise 185 | {:-deliver impl-deliver}) 186 | 187 | (extend-protocol p/IPromiseFactory 188 | clojure.lang.Fn 189 | (-promise [func] 190 | (let [promise (CompletableFuture.)] 191 | (schedule (fn [] 192 | (try 193 | (func #(p/-deliver promise %)) 194 | (catch Throwable e 195 | (p/-deliver promise e))))) 196 | promise)) 197 | 198 | Throwable 199 | (-promise [e] 200 | (let [p (CompletableFuture.)] 201 | (p/-deliver p e) 202 | p)) 203 | 204 | CompletionStage 205 | (-promise [cs] 206 | cs) 207 | 208 | Object 209 | (-promise [v] 210 | (let [p (CompletableFuture.)] 211 | (p/-deliver p v) 212 | p))) 213 | 214 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 215 | ;; Public Api 216 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 217 | 218 | ;; Constructors 219 | 220 | (defn promise 221 | "A promise constructor. 222 | 223 | This is a polymorphic function and this is a list of 224 | possible arguments: 225 | 226 | - throwable 227 | - plain value 228 | 229 | In case of the initial value is instance of `Throwable`, rejected 230 | promise will be retrned. In case of a plain value (not throwable), 231 | a resolved promise will be returned." 232 | ([] (CompletableFuture.)) 233 | ([v] (p/-promise v))) 234 | 235 | (defn resolved 236 | "Takes a value `v` and return a resolved promise 237 | with that value." 238 | [v] 239 | (let [cf (CompletableFuture.)] 240 | (.complete cf v) 241 | cf)) 242 | 243 | (defn rejected 244 | "Takes a error `e` and return a rejected promise 245 | with that error." 246 | [e] 247 | (let [cf (CompletableFuture.)] 248 | (.completeExceptionally cf e) 249 | cf)) 250 | 251 | (defmacro future 252 | "Takes a body of expressions and yields a promise object that will 253 | invoke the body in another thread. 254 | This is a drop in replacement for the clojure's builtin `future` 255 | function that return composable promises." 256 | [& body] 257 | `(let [suplier# (reify Supplier 258 | (get [_] 259 | ~@body))] 260 | (CompletableFuture/supplyAsync suplier# *executor*))) 261 | 262 | (defn promise? 263 | "Returns true if `p` is a promise 264 | instance." 265 | [p] 266 | (satisfies? p/IPromise p)) 267 | 268 | (defn done? 269 | "Returns true if promise `p` is 270 | done independently if successfully 271 | o exceptionally." 272 | [p] 273 | (p/-done? p)) 274 | 275 | (defn rejected? 276 | "Returns true if promise `p` is 277 | completed exceptionally." 278 | [p] 279 | (p/-rejected? p)) 280 | 281 | (defn resolved? 282 | "Returns true if promise `p` is 283 | completed successfully." 284 | [p] 285 | (p/-resolved? p)) 286 | 287 | (defn pending? 288 | "Returns true if promise `p` is 289 | stil in pending state." 290 | [p] 291 | (not (p/-done? p))) 292 | 293 | (defn deliver 294 | "Mark the promise as completed or rejected with optional 295 | value. 296 | 297 | If value is not specified `nil` will be used. If the value 298 | is instance of `Throwable` the promise will be rejected." 299 | ([p] 300 | (p/-deliver p nil)) 301 | ([p v] 302 | (p/-deliver p v))) 303 | 304 | (defn all 305 | "Given an array of promises, return a promise 306 | that is resolved when all the items in the 307 | array are resolved." 308 | [promises] 309 | (m/sequence (map p/-promise promises))) 310 | 311 | (defn any 312 | "Given an array of promises, return a promise 313 | that is resolved when first one item in the 314 | array is resolved." 315 | [promises] 316 | (->> (sequence (map p/-promise) promises) 317 | (into-array CompletableFuture) 318 | (CompletableFuture/anyOf))) 319 | 320 | (defn then 321 | "A chain helper for promises." 322 | [p callback] 323 | (p/-map p callback)) 324 | 325 | (defn chain 326 | "A variadic chain operation." 327 | [p & funcs] 328 | (reduce #(then %1 %2) p funcs)) 329 | 330 | (defn catch 331 | "Catch all promise chain helper." 332 | [p callback] 333 | (p/-catch p callback)) 334 | 335 | (defn branch 336 | [p callback errback] 337 | (-> p 338 | (p/-map callback) 339 | (p/-catch errback))) 340 | 341 | (defn reason 342 | "Get the rejection reason of this promise. 343 | Throws an error if the promise isn't rejected." 344 | [p] 345 | (let [e (m/extract p)] 346 | (when (instance? Throwable e) 347 | e))) 348 | 349 | (defn await 350 | ([^CompletionStage cs] 351 | (p/-await cs)) 352 | ([^CompletionStage cs ^long ms] 353 | (p/-await cs ms)) 354 | ([^CompletionStage cs ^long ms ^Object default] 355 | (p/-await cs ms default))) 356 | 357 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 358 | ;; Monad type implementation 359 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 360 | 361 | (def ^{:no-doc true} 362 | promise-context 363 | (reify 364 | mp/Context 365 | (-get-level [_] mc/+level-default+) 366 | 367 | mp/Functor 368 | (-fmap [mn f mv] 369 | (impl-map mv f)) 370 | 371 | mp/Applicative 372 | (-fapply [_ af av] 373 | (impl-map (all [af av]) 374 | (fn [[afv avv]] 375 | (afv avv)))) 376 | 377 | (-pure [_ v] 378 | (p/-promise v)) 379 | 380 | mp/Semigroup 381 | (-mappend [it mv mv'] 382 | (p/-map (m/sequence [mv mv']) 383 | (fn [[mvv mvv']] 384 | (let [ctx (mp/-get-context mvv)] 385 | (mp/-mappend ctx mvv mvv'))))) 386 | 387 | mp/Monad 388 | (-mreturn [_ v] 389 | (p/-promise v)) 390 | 391 | (-mbind [mn mv f] 392 | (impl-bind mv f)))) 393 | -------------------------------------------------------------------------------- /src/promissum/protocols.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright (c) 2015 Andrey Antukh 2 | ;; All rights reserved. 3 | ;; 4 | ;; Redistribution and use in source and binary forms, with or without 5 | ;; modification, are permitted provided that the following conditions 6 | ;; are met: 7 | ;; 8 | ;; 1. Redistributions of source code must retain the above copyright 9 | ;; notice, this list of conditions and the following disclaimer. 10 | ;; 2. Redistributions in binary form must reproduce the above copyright 11 | ;; notice, this list of conditions and the following disclaimer in the 12 | ;; documentation and/or other materials provided with the distribution. 13 | ;; 14 | ;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 15 | ;; IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | ;; OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | ;; IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | ;; INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | ;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | ;; DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 21 | ;; THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | ;; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 | ;; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | (ns promissum.protocols 26 | (:refer-clojure :exclude [promise deliver map await])) 27 | 28 | (defprotocol IState 29 | "Additional state related abstraction." 30 | (-rejected? [_] "Returns true if a promise is rejected.") 31 | (-resolved? [_] "Returns true if a promise is resolved.") 32 | (-done? [_] "Retutns true if a promise is already done.")) 33 | 34 | (defprotocol IFuture 35 | "A basic future abstraction." 36 | (-map [_ callback] "Chain a promise.") 37 | (-bind [_ callback] "Chain a promise.") 38 | (-catch [_ callback] "Catch a error in a promise.")) 39 | 40 | (defprotocol IAwaitable 41 | (-await 42 | [awaitable] 43 | [awaitable ms] 44 | [awaitable ms default])) 45 | 46 | (defprotocol IPromise 47 | "A basic promise abstraction." 48 | (-deliver [_ value] "Deliver a value into promise.")) 49 | 50 | (defprotocol IPromiseFactory 51 | "A promise constructor abstraction." 52 | (-promise [_] "Create a promise instance.")) 53 | -------------------------------------------------------------------------------- /test/promissum/core_tests.clj: -------------------------------------------------------------------------------- 1 | (ns promissum.core-tests 2 | (:require [clojure.test :refer :all] 3 | [cats.core :as m] 4 | [cats.protocols :as mp] 5 | [cats.builtin :as mb] 6 | [promissum.core :as p])) 7 | 8 | (deftest constructors-tests 9 | (testing "Promise can be created from value." 10 | (let [p1 (p/promise 1)] 11 | (is (p/promise? p1)) 12 | (is (p/resolved? p1)) 13 | (is (not (p/rejected? p1))) 14 | (is (not (p/pending? p1))))) 15 | 16 | (testing "Promise can be created without value." 17 | (let [p1 (p/promise)] 18 | (is (p/promise? p1)) 19 | (is (not (p/resolved? p1))) 20 | (is (not (p/rejected? p1))) 21 | (is (p/pending? p1)))) 22 | 23 | (testing "Promise can be created from exception." 24 | (let [p1 (p/promise (ex-info "" {}))] 25 | (is (p/promise? p1)) 26 | (is (not (p/resolved? p1))) 27 | (is (p/rejected? p1)) 28 | (is (not (p/pending? p1))))) 29 | 30 | (testing "Creating promise from promise." 31 | (let [p1 (p/promise) 32 | p2 (p/promise p1)] 33 | (is (identical? p1 p2)))) 34 | 35 | (testing "Delivery with factory" 36 | (let [p (p/promise (fn [deliver] 37 | (deliver 2)))] 38 | (is (= @p 2)))) 39 | 40 | (testing "A `resolved` constructor." 41 | (is (= 2 @(p/resolved 2)))) 42 | 43 | (testing "A `rejected` constructor with await" 44 | (let [exc (ex-info "foo" {:bar :baz})] 45 | (is (thrown? clojure.lang.ExceptionInfo 46 | (p/await (p/rejected exc)))))) 47 | 48 | (testing "Future replacement" 49 | (is (= 3 @(future (+ 1 2))))) 50 | ) 51 | 52 | (deftest operations-tests 53 | (testing "Promise Extract" 54 | (let [p1 (p/promise 1)] 55 | (is (= 1 @p1))) 56 | 57 | (let [p1 (p/promise (ex-info "foobar" {:foo 1}))] 58 | (is (= "foobar" (.getMessage (m/extract p1)))) 59 | (try 60 | @p1 61 | (catch Exception e 62 | (is (= {:foo 1} (.. e getCause getData))))))) 63 | 64 | (testing "Simple delivering" 65 | (let [p1 (p/promise) 66 | _ (p/future 67 | (p/deliver p1 2))] 68 | (is (= 2 @p1)))) 69 | 70 | (testing "Chaining using then" 71 | (let [p1 (p/future 72 | (Thread/sleep 200) 73 | 2) 74 | p2 (p/then p1 inc) 75 | p3 (p/then p2 inc)] 76 | (is (= 4 @p3)))) 77 | 78 | (testing "Chaining using chain" 79 | (let [p1 (p/future 80 | (Thread/sleep 200) 81 | 2) 82 | p2 (p/chain p1 inc inc inc)] 83 | (is (= 5 @p2)))) 84 | 85 | (testing "Deref rejected promise" 86 | (let [p1 (p/future 87 | (throw (ex-info "foobar" {})))] 88 | (is (thrown? java.util.concurrent.ExecutionException @p1)))) 89 | 90 | (testing "Await rejected promise" 91 | (let [p1 (p/future 92 | (throw (ex-info "foobar" {})))] 93 | (is (thrown? clojure.lang.ExceptionInfo (p/await p1))))) 94 | 95 | (testing "Reject promise in the middle of chain" 96 | (let [p1 (p/future 1) 97 | p2 (p/then p1 (fn [v] 98 | (throw (ex-info "foobar" {:msg "foo"})))) 99 | p3 (p/catch p2 (fn [e] 100 | (:msg (.getData e))))] 101 | (is (= "foo" @p3)))) 102 | 103 | (testing "Synchronize two promises." 104 | (let [p1 (p/all [(p/promise 1) (p/promise 2)])] 105 | (is (= @p1 [1 2])))) 106 | 107 | (testing "Arbitrary select first resolved promise" 108 | (let [p1 (p/any [(p/promise 1) (p/promise (ex-info "" {}))])] 109 | (is (= @p1 1)))) 110 | ) 111 | 112 | (deftest cats-tests 113 | (testing "Promise is a functor." 114 | (let [p (m/pure p/promise-context 1)] 115 | (is (= 2 @(m/fmap inc p))))) 116 | 117 | (testing "Promise is a monad" 118 | (let [p (m/pure p/promise-context 1)] 119 | (is (= 2 @(m/>>= p (fn [x] (m/return (inc x))))))) 120 | 121 | (let [p1 (p/future 1) 122 | p2 (p/future 1) 123 | p3 (p/future 1) 124 | r (m/mlet [x p1 125 | y p2 126 | z p3] 127 | (m/return (+ x y z)))] 128 | (is (= 3 @r)))) 129 | 130 | (testing "First monad law: left identity" 131 | (let [p1 (m/pure p/promise-context 4) 132 | p2 (m/pure p/promise-context 4) 133 | vl (m/>>= p2 #(m/pure p/promise-context %))] 134 | (is (= (p/await p1) 135 | (p/await vl))))) 136 | 137 | (testing "Second monad law: right identity" 138 | (let [p1 (p/future 3) 139 | rs (m/>>= (p/future 3) m/return)] 140 | (is (= @p1 @rs)))) 141 | 142 | (testing "Third monad law: associativity" 143 | (let [rs1 (m/>>= (m/mlet [x (p/future 2) 144 | y (p/future (inc x))] 145 | (m/return y)) 146 | (fn [y] (p/future (inc y)))) 147 | rs2 (m/>>= (p/future 2) 148 | (fn [x] (m/>>= (p/future (inc x)) 149 | (fn [y] (p/future (inc y))))))] 150 | (is (= @rs1 @rs2)))) 151 | 152 | (testing "Primise as semigroup" 153 | (let [c1 (p/promise {:a 1}) 154 | c2 (p/promise {:b 2}) 155 | r (m/mappend c1 c2)] 156 | (is (= {:a 1 :b 2} @r))) 157 | (let [c1 (p/promise {:a 1}) 158 | c2 (p/promise (ex-info "" {:b 2})) 159 | r (m/mappend c1 c2)] 160 | (is (thrown? clojure.lang.ExceptionInfo (p/await r))))) 161 | 162 | (testing "Use promises in applicative do syntax" 163 | (letfn [(async-call [wait] 164 | (p/future 165 | (Thread/sleep 100) 166 | wait))] 167 | (let [result (m/alet [x (async-call 100) 168 | y (async-call 100)] 169 | (+ x y))] 170 | (is (p/promise? result)) 171 | (is (= @result 200))))) 172 | ) 173 | --------------------------------------------------------------------------------