In instances where I want to mess with the control flow and define custom processing, I have used macros like the following (although this is a toy/thought piece without the customary testing). It will act like the ->
threading macro, except we introduce two new functions invalid
and the predicate invalid?
to detect invalid results. It also takes an initial pair of args: a predicate to detect/mark invalid intermediate results, and a handler function to do something in the face of invalidity (either repair it or return something wrapped in invalid
which yields an Invalid
wrapper object that is dereffable like an atom).
So the general idea is to have these sequential intermediate computations, and if we detect invalidity (per the predicate arg), we invoke the handler. If any result (even after passing the handler) yields an Invalid instance (as constructed by the invalid
function), we stop computing (akin to some->) and yield the invalid result. Otherwise, the pipeline behaves as ->
would, threading the prior result as the first arg of the successive form evaluation.
(deftype Invalid [obj]
clojure.lang.IDeref
(deref [this] obj))
(defn invalid? [x] (instance? Invalid x))
(defn invalid [x] (Invalid. x))
(defmacro valid-> [[pred handler] & forms]
(let [res (gensym "res")
p (gensym)
h (gensym)
steps (for [form (rest forms)]
`(let [this# (if (invalid? ~res)
~res
(-> ~res ~form))
res# (if (~p this#)
(~h this#)
this#)]
res#))]
`(let [~p ~pred
~h ~handler
~res ~(first forms)
~res (if (~p ~res)
(~h ~res)
~res)
~@(interleave (repeat res) (butlast steps))]
~(if (empty? steps)
res
(last steps)))))
If we apply it to a similar example from the python code, we can transform a map and assoc values to it. We detect invalidity by the predicate form of :error
, e.g. (:error some-map)
which is equivalently (get some-map :error)
since keywords have that functional semantic. The handler function will print that there is an error, and yield an invalid
result with the current value:
user> (valid-> [:error
(fn [d]
(println ["error!" d])
(invalid d))]
{:a "hello"}
(assoc :b "world")
(assoc :error "oh no!")
(assoc :d "shouldn't get here!"))
[error! {:a hello, :b world, :error oh no!}]
#<Invalid@4d9d86ab: {:a "hello", :b "world", :error "oh no!"}>
We can hook in anything for the pred/handler functions, and yield some exception info (or even throw if we want to):
user> (valid-> [:error
(fn [d]
(invalid (ex-info "bad-input!" {:data d})))]
{:a "hello"}
(assoc :b "world")
(assoc :error "oh no!")
(assoc :d "shouldn't get here!"))
#<Invalid@2016241e: #error {
:cause "bad-input!"
:data {:data {:a "hello", :b "world", :error "oh no!"}}
:via
[{:type clojure.lang.ExceptionInfo
:message "bad-input!"
:data {:data {:a "hello", :b "world", :error "oh no!"}}
:at [user$eval14906$G__14905__14907 invoke "form-init5309770136055223389.clj" 402]}]
:trace
[[user$eval14906$G__14905__14907 invoke "form-init5309770136055223389.clj" 402]
[user$eval14906 invokeStatic "form-init5309770136055223389.clj" 400]
[user$eval14906 invoke "form-init5309770136055223389.clj" 400]
[clojure.lang.Compiler eval "Compiler.java" 7194]
[clojure.lang.Compiler eval "Compiler.java" 7149]
...elided]}>
This mostly lifted from the implementation of clojure.core/some->:
user> (use 'clojure.repl)
nil
user> (source some->)
(defmacro some->
"When expr is not nil, threads it into the first form (via ->),
and when that result is not nil, through the next etc"
{:added "1.5"}
[expr & forms]
(let [g (gensym)
steps (map (fn [step] `(if (nil? ~g) nil (-> ~g ~step)))
forms)]
`(let [~g ~expr
~@(interleave (repeat g) (butlast steps))]
~(if (empty? steps)
g
(last steps)))))
nil
You could accomplish something simpler where flow-control is determined by the presence of exceptions (or use the built-in clojure.core/reduced to indicate termination, or use metadata).
You could implement a similar processing pipeline with functions and custom reduce
too (if there is a validation error, just indicate it and return a reduced
result to terminate processing), although it’s a bit more paperwork to express the pipeline.