Wouldn't it be nice if if-let allowed more bindings?
Try this, which I hereby dedicate into the public domain so that anyone may
use it freely in their code without restrictions:
(defn if-and-let*
[bindings then-clause else-clause deshadower]
(if (empty? bindings)
then-clause
`(if-let ~(vec (take 2 bindings))
~(if-and-let* (drop 2 bindings) then-clause else-clause deshadower)
(let ~(vec (apply concat deshadower))
~else-clause))))
(defmacro if-and-let
"Like if-let, but with multiple bindings allowed. If all of the
expressions in
the bindings evaluate truthy, the then-clause is executed with all of the
bindings in effect. If any of the expressions evaluates falsey,
evaluation of
the remaining binding exprs is not done, and the else-clause is executed
with
none of the bindings in effect. If else-clause is omitted, evaluates to
nil
if any of the binding expressions evaluates falsey.
As with normal let bindings, each binding is available in the subsequent
bindings. (if-and-let [a (get my-map :thing) b (do-thing-with a)] ...) is
legal, and will not throw a null pointer exception if my-map lacks a
:thing
key and (do-thing-with nil) would throw an NPE.
If there's something you want to be part of the then-clause's condition,
but
whose value you don't care about, including a binding of it to _ is more
compact than nesting yet another if inside the then-clause."
([bindings then-clause]
`(if-and-let ~bindings ~then-clause nil))
([bindings then-clause else-clause]
(let [shadowed-syms (filter #(or ((or &env {}) %) (resolve %))
(filter symbol?
(tree-seq coll? seq (take-nth 2 bindings))))
deshadower (zipmap shadowed-syms (repeatedly gensym))]
`(let ~(vec (apply concat (map (fn [[k v]] [v k]) deshadower)))
~(if-and-let* bindings then-clause else-clause deshadower)))))
=> (if-and-let [x (:a {:b 42}) y (first [(/ x 3)])] [x y] :nothing)
:nothing
=> (if-and-let [x (:a {:a 42}) y (first [(/ x 3)])] [x y] :nothing)
[42 14]
=> (if-and-let [x (:a {:a 42}) y (first [])] [x y] :nothing)
:nothing
Note that this is not quite as simple as the obvious naive implementation:
(defmacro naive-if-and-let
([bindings then-clause]
`(naive-if-and-let ~bindings ~then-clause nil))
([bindings then-clause else-clause]
(if (empty? bindings)
then-clause
`(if-let ~(vec (take 2 bindings))
(naive-if-and-let ~(vec (drop 2 bindings))
~then-clause
~else-clause)
~else-clause))))
but what happens if a name used in the if-and-let is already bound in the
enclosing context is instructive:
=> (let [x 6] (if-and-let [x (:a {:b 42}) y (first [(/ x 3)])] [x y] x))
6
=> (let [x 6] (if-and-let [x (:a {:a 42}) y (first [])] [x y] x))
6
=> (let [x 6] (naive-if-and-let [x (:a {:b 42}) y (first [(/ x 3)])] [x y]
x))
6
=> (let [x 6] (naive-if-and-let [x (:a {:a 42}) y (first [])] [x y] x))
42
As you can see, the x in the else clause in naive-if-and-let sometimes sees
the x binding in the if-and-let (if that succeeded) and sometimes sees the
enclosing binding (if not), when it should always refer to the enclosing
(let [x 6] ...). The non-naive if-and-let discovers all local bindings that
might be shadowed by walking the left hand sides of the new bindings and
tree-walking the data structure there to extract symbols, which it filters
further against &env. It outputs an enclosing let that saves all of these
to non-shadowed locals named with gensyms, and wraps every else clause
emission in a let that restores the original bindings of these symbols from
these gensym locals. The tree-walking makes it work even with destructuring
its the binding vector:
=> (let [x 6] (if-and-let [{x :a} {:a 42} y (first [(/ x 3)])] [x y] x))
[42 14]
=> (let [x 6] (if-and-let [{x :a} {:a 42} y (first [])] [x y] x))
6
It also unshadows defs:
=> (def x 6)
=> (if-and-let [{x :a} {:a 42} y (first [])] [x y] x)
6
That's from the (or ... (resolve %)) part of the outer filter on the walked
tree. Remove that and leave the outer filter as just (filter (or &env {})
...), and that last test produces 42 instead.
Not that you should really be shadowing defs with locals anyway. That's
always prone to cause problems.
Not bad for only 18 lines of actual code, hmm?
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to [email protected]
Note that posts from new members are moderated - please be patient with your
first post.
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups
"Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.