The Pact library for Clojure
(This is a copy of the readme file from the repository.)
Pact is a small library for chaining values through forms. It’s like a promise but much simpler.
Installation
Lein:
[com.github.igrishaev/pact "0.1.0"]
Deps.edn
{com.github.igrishaev/pact {:mvn/version "0.1.0"}}
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 them
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
. 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
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.
Infernally, the then
handler calls for the .thenApply
method if a future and
the error
handler boils down to .exceptionally
.
Manifold
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
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 ..."
Testing
To run the tests, do lein test
or just make test
.
© 2022 Ivan Grishaev
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter
You Are Here, 15th Feb 2022, link
Looks interesting, somewhat similar to https://github.com/fmnoise/...
Ivan Grishaev, 16th Feb 2022, link , parent
Yes, Pact is quite close to fmnoise/flow, but it's even more simpler.