Reputation: 3568
Let us suppose we are supposed to do an series of checks on email address and the data that is associated with the email:
Check if the email is nil ? if nil return {:msg "emial is nil"}
check if email has an account (i.e do a db lookup of the account for email). if account is nil , return {:msg "no account"}
check if the account is in expired. if accout is not in expired , return {:msg expired}
.. and so on
Currrently, I have coded this as nested if-let statements:
(if-let [email (if (nil? email) nil email)]
(if-let [acct (load-act email)]
(if-let [goodacct (if (expired? acct) nil acct)]
....
))
is there a nicer way or more idiomatic way to write this ?
Upvotes: 1
Views: 117
Reputation: 3346
Here's another approach: you model your user account as a plain map, and you define some basic validation functions. If it's just the first error that you are interested in, you can map
the application of different functions across the same user account data, and pick the first error.
You can enhance this to collect all of the errors in a vector, or augmenting your map (eg. with reduce
) so steps further down on the validation pipeline can re-use data obtained from side-effects from prior stages (eg. if a prior step went to the db, maybe assoc
more keys into the map so that later validations can re-use the same data without extra queries. You'd need to change the validation functions to take a map and return a pair of [updated-map error]
).
A quick and dirty example:
(defn valid-email [{:keys [email]}]
(when-not email "email is nil"))
(defn valid-account [{:keys [account]}]
(when-not account "no account"))
(defn expired-account [{:keys [account]}]
(when (:expired account) "account expired"))
(def validations [valid-email valid-account expired-account])
(defn validator [user-data]
(when-let [error (some identity (map #(% user-data) validations))]
{:msg error}))
;; (validator {})
;; => {:msg "email is nil"}
;; (validator {:email "[email protected]"})
;; => {:msg "no account"}
;; (validator {:email "[email protected]" :account {:expired true}})
;; => {:msg "account expired"}
;; (validator {:email "[email protected]" :account {:expired false}})
;; => nil ;; nil means "no validation errors"
Upvotes: 1
Reputation: 9865
The same like @xificurC's last version. But why not use normal syntax? (no need for unnecessary dependencies ;) ). Is not that more typing:
(cond (nil? email) {:msg "email is nil"}
:else (let [acct (load-act email)]
(cond (nil? acct) {:msg "no account"}
(expired? acct) {:msg "expired account"}
:else acct)))
Of course, one could discuss whether first cond
should be an if
.
But I like cond
, because it lays down horizontally cause and effect on the left and right hand.
And the logic flows down like a waterfall but exits as soon as a condition is met.
Upvotes: 1
Reputation: 1178
There are several ways to factor this:
1) move the nil-check on e-mail higher up your chain. Then your function can do
(let [acct (load-act email)]
(cond (nil? acct) "no account"
(expired? acct) "account expired"
:else acct))
2) not all of your tests really need to keep the result. The if-lets can be simplified to
(if (nil? email)
"email is nil"
(if-let [acct (load-act email)]
(if (expired? acct)
"account expired"
acct)
"no account"))
A side-note - clojure's notion of false-ness is nil
and false
. You're using a nil?
check which really checks for nil
only, and if-let
which checks for false
too. There's if-some
which only does the nil
check.
3) better-cond
allows binding inside its cond construct:
(b/cond
(nil? email) "email is nil"
:let [acct (load-act email)]
(nil? acct) "no account"
(expired? acct) "account expired"
:else acct)
Upvotes: 2