Simple, functional and value-oriented concurrency primitives for Clojure.
- Value-oriented: tasks are just eventual values. No more callbacks, regular
deref
/@
is all you need. - Functional: tasks are composable. Tasks come with a set of operations that lets you compose and combine them in a functional manner.
- Interoperable: Any Clojure
future
/promise
can be converted into a task, and vice versa. - Asynchronous: tasks are always asynchronous. Tasks default to the ForkJoinPool executor.
- Customizable: If you wish to control the execution model, you can define your own ExecutorService.
- Performant: The library leverages the standard Java 8 Concurrency API, a powerful framework for concurrent computation.
The task API is built on basic building blocks, run
, then
, compose
and for
.
Use the standard deref
/@
to block the current thread and await the result.
(def my-var (task/run 123))
@(my-var) ; => 123
then
applies a function to the result of another task.
(def async-var (task/run (Thread/sleep 1000)
"asdf"))
@(task/then str/upper-case async-var)
; => "ASDF"
compose
applies a function producing a task on the result of a a task, and chains their execution together.
(defn comp1 [x]
(task/run (Thread/sleep 1000)
(inc x)))
(defn comp2 [x]
(task/run (Thread/sleep 1000)
(* 2 x)))
@(task/compose comp2 (comp1 4))
; => 10
for
lets you apply compose
and then
without the boilerplate. It behaves like let
, it binds
the symbols in the bindings to futures. Once all futures are complete, it evaluates the body.
@(task/for [x (future 1)
y (task/run 2)
c (task/run (Thread/sleep 1000)
7)]
(+ x y c))
; => 10
In the example below, the promise returned by http/get
is automatically converted into a task. The
function then extracts the body, parses it into JSON, gets the title and uppercases it. This
executes asynchronously in another thread, so we have to deref it to print its results.
(def request
(task/then
(fn [data] (-> data
:body
(cheshire/parse-string true)
:title
str/upper-case))
(http/get "http://jsonplaceholder.typicode.com/posts/3")))
(println @request)
Here we chain two HTTP requests together. First we POST to api-url
with some example data, then
we extract the Location
header, and execute a GET request to that URL. By using compose we don't
end up with nested tasks.
(defn post-and-fetch
[title content]
(task/compose
(fn [response]
(http/get (-> response :headers :location)))
(http/post api-url
{:body (cheshire/generate-string {:userId 123 :title title :body content})})))
@(post-and-fetch "Sample title" "Sample content!")
;; => {:opts {...}, :body "this would be the result", :headers {...}, :status 200}
See Error handling in the user guide
task provides advanced facilities for error handling. Currently the principal method is recover
which accepts a task and a function. The function is passed any exception thrown from the task and
then the value returned by the function. recover
produces a new task:
(def boom (task/run (/ 1 0)))
(def incremented (task/then inc boom))
@(task/recover incremented
(fn [ex]
(println "caught exception: " (.getMessage ex))
123))
;; => 123
Copyright © Antoine Kalmbach. All rights reserved.
Distributed under the Apache License either version 2.0 or (at your option) any later version.