railway oriented programming for clojure & clojurescript, a functional approach to error handling.
fatum starting as a friendly copy of fmnoise/flow, but it went in a slightly different direction to suit my personal needs.
handling exceptions in a functional way, is a non-trivial problem. one way to solve it might be the railway oriented programming (rop) principle, widely used in other functional languages, but not very often (too rarely!) found in clojure.
A program is a spell cast over a computer, turning input into error messages.
(defn create-user
"completely meaningless but well nested code"
[req]
(cond
(= 200 (:status req))
(let [user (make-user (:body req))]
(if (valid-user? user)
(let [db-repsonse (update-db :add-user user)]
(if (:success? db-reponse)
db-response
(throw (ex-info "failed updating db" {:user user :message (:message db-response)}))))
(throw (ex-info "invalid user" {:user user}))))
(= 500 (:status req))
(throw (ex-info "something went wrong"))
(= 404 (:status req))
(throw (ex-info "something went even worse"))))
sufficiently complex, repeatedly revised and refactored code tends towards an infinite number of if-else conditions.
it is worth noting that exception handling in java is slow, very slow. using a custom type for exceptions allows the stacktrace to be omitted. throwing also involves an additional cost, the passing of value is much cheaper and faster. creating a fail is ~50x faster than creating an exception.
the main idea behind fatum, as behind fmnoise/flow, is to separate values from errors. exceptions are first class citizens in java/javascript and there is no need for additional abstractions. each value can be either ok?, either fail?.
Fail is a class inheriting from ExceptionInfo, which also behaves like a hashmap, allowing you to instantly check the contents of ex-data as well as add values to it. get, assoc, keys, values, seq, all tricks allowed.
in addition, to improve performance, the stacktrace, which is completely unnecessary in this case, is not included. creating a Fail as opposed to throwing an Exception is immediate and costless.
this is similar to the solution found in failjure. executes a body in try/catch context and, if an exception is thrown, exception is turned into Fail and returned as a value.
(f/attempt (/ 1 0))
;; => #error {
;; :cause "Divide by zero"
;; :data {}
;; :via
;; [{:type ribelo.fatum.Fail
;; :message "Divide by zero"
;; :data {}}]
;; :trace
;; []}
the most natural and idiomatic way to define a path is to use ->, a similar solution can be found in js, where promises are handled with an endless sequence of then.
then attempt to call function f when value is not meet fail? pred, otherwise bypass.
(-> 1
(f/then inc)
(f/then inc)
(f/then inc)
(f/then inc))
;; => 5
trying to use -> together with try/catch will inevitably lead to a mess. again, js comes to the rescue, which implements a catch for promises.
catch attempt to call function f when value meets fail? pred, otherwise bypass.
(-> 1
(f/then (fn [x] (/ x 0)))
(f/catch (fn [err] (assoc err :code 418))))
;; => #error {
;; :cause "Divide by zero"
;; :data {:code 418}
;; :via
;; [{:type ribelo.fatum.Fail
;; :message "Divide by zero"
;; :data {:code 418}}]
;; :trace
;; []}
the world is not pure and sometimes you just have to.
thru attempt to call function f, bypassing value unchanged
(-> 1 (f/then inc) (f/thru println))
;; => prints 2
;; => return 2
(-> 1 (f/then (fn [x] (/ x 0))) (f/thru println))
;; prints & return
;; => #error {
;; :cause "Divide by zero"
;; :data {}
;; :via
;; [{:type ribelo.fatum.Fail
;; :message "Divide by zero"
;; :data {}}]
;; :trace
;; []}
(defn create-user
"completely meaningless but well nested code"
[req]
(-> req
(f/fail-if {:staus 500} "something went wrong")
(f/fail-if {:staus 404} "something went even worse")
(f/then-if {:status 200} (comp make-user :body))
(f/fail-if (complement valid-user?) "invalid user" (partial array-map :user))
(f/then (partial update-db :add-user))
(f/fail-if (complement :success?) #(find % :message))
(f/maybe-throw)))
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |