pact
pact copied to clipboard
Chaining values with ease
Pact
A small library for chaining values through forms. It's like a promise but much simpler.
Since 0.1.1, supports ClojureScript and its specific types (e.g. Promise).
Installation
Lein:
[com.github.igrishaev/pact "0.1.1"]
Deps.edn
{com.github.igrishaev/pact {:mvn/version "0.1.1"}}
How It Works
The library declares two universe handlers: then and error. When you apply
them to the "good" values, you propagate further. Applying the error for does
nothing. And vice versa: then for the "bad" values does nothing, but calling
error on "bad" values gives you a chance to recover the pipeline.
By default, there is only one "bad" value which is an instance of Throwable
(js/Error in ClojureScript). Other types are considered positive ones. The
library carries extensions for such async data types as CompletableFuture,
Manifold and core.async. You only need to require their modules so they
extend the IPact protocol.
Examples
Import then and error macros, then chain a value with the standard ->
threading macro. Both then and error accept a binding vector and an
arbitrary body.
(ns foobar
(:require
[pact.core :refer [then error]]))
(-> 42
(then [x]
(-> x int str))
(then [x]
(str x "/hello")))
"42/hello"
If any exception pops up, the sequence of then handlers gets interrupted, and
the error handler gets into play:
(-> 1
(then [x]
(/ x 0))
(then [x]
(str x "/hello")) ;; won't be executed
(error [e]
(ex-message e)))
"Divide by zero"
The error handler gives you a chance to recover from the exception. If you
return a non-exceptional data in error, the execution will proceed from the
next then handler:
(-> 1
(then [x]
(/ x 0))
(error [e]
(ex-message e))
(then [message]
(log/info message)))
;; nil
The -> macro can be nested. This is useful to capture the context for a
possible exception:
(-> 1
(then [x]
(+ x 1))
(then [x]
(-> x
(then [x]
(/ x 0))
(error [e]
(println "The x was" x)
nil))))
;; The x was 2
;; nil
Besides then and error macros, the library provides the then-fn and
error-fn functions. They are useful when you have a ready function that
processes the value:
(ns foobar
(:require
[pact.core :refer [then-fn error-fn]]))
(-> 1
(then-fn inc)
(then-fn str))
;; "2"
(-> 1
(then [x]
(/ x 0))
(error-fn ex-message))
;; "Divide by zero"
Chaining with then and error is especially good for maps as allowing
destructuring:
(-> {:db {...} :cassandra {...}}
;; Get a user from the database and attach it to the scope.
(then [{:as scope :keys [db]}]
(let [user (jdbc/get-by-id db :users 42)]
(assoc scope :user user)))
;; Having a user, get their last items from Cassandra cluster
;; and attach them to the scope.
(then [{:as scope :keys [cassandra user]}]
(let [items (get-user-items cassandra user)]
(assoc scope :items items)))
;; Do something more...
(then [...]
...))
Fast fail
To interrupt the chain of then handlers, either throw an exception or use the
failure function which is just a shortcut for raising a exception. The
function takes a map or a message with a map:
(ns foobar
(:require
[pact.core :refer [then error failure]]))
(-> 1
(then [x]
(if (not= x 42)
(failure "It was not 42!" {:x x})
(+ 1 x)))
(error-fn ex-data))
;; {:x 1 :ex/type :pact.core/failure}
Supported types
The core namespace declares the then and error handlers for the Object,
Throwable, and java.util.concurrent.Future types. The Future values get
dereferenced when passing to then.
The following modules extend the IPact protocol for asynchronous types.
Completable Future (Clojure)
The module pact.comp-future handles the CompletableFuture class available
since Java 11. The module also provides its own future macro to build an
instance of CompletableFuture:
(-> (future/future 1)
(then [x]
(inc x))
(then [x]
(/ 0 0))
(error [e]
(ex-message e))
(deref))
"Divide by zero"
Pay attention: if you fed an instance of CompletableFuture to the threading
macro, the result will always be of this type. Thus, there is a deref call at
the end.
Internally, the then handler calls for the .thenApply method if a future and
the error handler boils down to .exceptionally.
Manifold (Clojure)
The pact.manifold module makes the handlers work with the amazing Manifold
library and its types. The Pact library doesn't have Manifold dependency: you've
got to add it on your own.
[manifold "0.1.9-alpha3"]
(-> (d/future 1)
(then [x]
(/ x 0))
(error [e]
(ex-message e))
(deref))
"Divide by zero"
Under the hood, then and error handlers call the d/chain and d/catch
macros respectively.
Once you've put an instance of Manifold deferred, the result will always be a
Deferred.
Core.async (Clojure + ClojureScript)
To make the library work with core.async channels, import the
pact.core-async module:
(ns foobar
(:require
[pact.core :refer [then error]]
[pact.core-async]
[clojure.core.async :as a]))
Like Manifold, the core.async dependency should be added by you as well:
[org.clojure/core.async "1.5.648"]
Now you can chain channels through the then and error actions. Internally,
each handler takes exactly one value from a source channel and returns a new
channel with the result. For then, exceptions traverse the channels being
untouched. And instead, the error handler ignores ordinary values and affects
only exceptions. Quick demo:
(let [in (a/chan)
out (-> in
(then [x]
(/ x 0))
(error [e]
(ex-message e))
(then [message]
(str "<<< " message " >>>")))]
(a/put! in 1)
(a/<!! out) )
;; "<<< class java.lang.String cannot be cast ..."
JS Promise (ClojureScript)
For a JS promise, then and error handlers resolve to its .then and
.catch methods:
(-> (js/Promise.resolve 1)
(then-fn inc)
(then [x]
(js/console.log x)))
A better example with fetching an HTTP resource:
(-> (js/fetch "https://some.api.com/data.json")
(then [response]
(.json response))
(then [data]
...)
(error [e]
(js/console.log ...)))
Testing
To run both Clojure and ClojureScript tests, execute make test-all. For the
ClojureScript tests, you need Node.js installed.
© 2022 Ivan Grishaev