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 clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
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 clojure+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to