I have a Clojure function that looks something like the following.
(defn calculate-stuff [data]
(if (some-simple-validation data)
(create-error data)
(let [foo (calculate-stuff-using data)]
(if (failed? foo)
(create-error foo)
(let [bar (calculate-more-stuff-using foo)]
(if (failed? bar)
(create-error bar)
(calculate-response bar)))))))
Which works fine but is a little hard to read, so I was wondering if there was a more idiomatic Clojure way of writing this?
I thought about making some-simple-validation
, calculate-stuff-using
and calculate-more-stuff-using
throw exceptions and using a try/catch block but that felt like using exceptions for control flow which didn't feel correct.
I can't let the exceptions escape this function either as I'm using it to map a seq of maps and I still want to continue processing the remainder.
I guess what I'm after is something like this?
(defn calculate-stuff [data]
(let-with-checking-function
[valid-data (some-simple-validation data)
foo (calculate-stuff-using valid-data)
bar (calculate-more-stuff-using foo)]
failed?) ; this function is used to check each variable
(create-error %) ; % is the variable that failed
(calculate-response bar)) ; all variables are OK
Thanks!
If a failed validation indicates an error condition, an exception (and a try-catch block) may be the best way of handling it. Especially if it is not a "normal" occurrance (i.e. invalid cust-id, etc).
For more "normal" but still "invalid" cases, you might use some->
(pronounced "some-thread") to quietly squelch "bad" cases. Just have your validators return nil
for bad data, and some->
will abort the processing chain:
(defn proc-num [n]
(when (number? n)
(println :proc-num n)
n))
(defn proc-int [n]
(when (int? n)
(println :proc-int n)
n))
(defn proc-odd [n]
(when (odd? n)
(println :proc-odd n)
n))
(defn proc-ten [n]
(when (< 10 n)
(println :proc-10 n)
n))
(defn process [arg]
(when (nil? arg)
(throw (ex-info "Cannot have nil data" {:arg arg})))
(some-> arg
proc-num
proc-int
proc-odd
proc-ten))
results:
(process :a) => nil
(process "foo") => nil
:proc-num 12
:proc-int 12
(process 12) => nil
:proc-num 13
:proc-int 13
:proc-odd 13
:proc-10 13
(process 13) => 13
(throws? (process nil)) => true
Having said this, you are now using nil
to mean "data validation failure", so you cannot have nil
in your data.
Using nil
as a special value to short-circuit processing can work, but it might be easier to use plain-old exceptions, especially for cases that are clearly "bad data":
(defn parse-with-default [str-val default-val]
(try
(Long/parseLong str-val)
(catch Exception e
default-val))) ; default value
(parse-with-default "66-Six" 42) => 42
I have a little macro to automate this process called with-exception-default
:
(defn proc-num [n]
(when-not (number? n)
(throw (IllegalArgumentException. "Not a number")))
n)
(defn proc-int [n]
(when-not (int? n)
(throw (IllegalArgumentException. "Not int")))
n)
(defn proc-odd [n]
(when-not (odd? n)
(throw (IllegalArgumentException. "Not odd")))
n)
(defn proc-ten [n]
(when-not (< 10 n)
(throw (IllegalArgumentException. "Not big enough")))
n)
(defn process [arg]
(with-exception-default 42 ; <= default value to return if anything fails
(-> arg
proc-num
proc-int
proc-odd
proc-ten)))
(process nil) => 42
(process :a) => 42
(process "foo") => 42
(process 12) => 42
(process 13) => 13
This avoids giving a special meaning to nil
or any other "sentinal" value, and uses Exception
for its normal purpose of altering control flow in the presence of errors.