Skip to content

Instantly share code, notes, and snippets.

@pingles
Last active July 26, 2016 23:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pingles/5150585 to your computer and use it in GitHub Desktop.
Save pingles/5150585 to your computer and use it in GitHub Desktop.
Clojure if-let accepting multiple bindings
(ns if-let-multi-bind.core)
(defmacro if-let*
([bindings then]
`(if-let* ~bindings ~then nil))
([bindings then else & oldform]
(let [test (cons #'and (map last (partition 2 bindings)))]
`(if ~test
(let ~bindings
~then)
~else))))
(comment
(if-let* [a 1
b 3]
(+ a b))
;; 4
(if-let* [a 1
b nil]
(+ a b))
;; nil
(def x 1)
(def y false)
(if-let* [a x
b y]
[a b])
;; nil
)
@ragnard
Copy link

ragnard commented Mar 13, 2013

I think this might be sligthly wrong, since the tests are evaluated at expansion time, rather than runtime:

(macroexpand '(if-let* [a 1 b 2] (+ a b)))

;; => (if true (clojure.core/let [a 1 b 2] (+ a b)) nil)

I think the problem is ~(every? identity tests)

This has the following consequence:

(def x 1)
(def y false)

(if-let* [a x
          b y]
         [a b])

;; =>  [1 false]

Also, the tests will be evaluated twice.

@ragnard
Copy link

ragnard commented Mar 13, 2013

Couldn't help myself so I had a go at using nested if-lets as you proposed. Not nearly as concise though.

(defn if-let-helper*
  [bindings then else]
  (let [pairs  (reverse (partition 2 bindings))
        top    (last pairs)]
    `(if-let ~(vec top) 
       ~(reduce (fn [res [name test]]
                  `(if-let [~name ~test] ~res))
                `~then
                (butlast pairs))
       ~else)))

(defmacro if-let*
  ([bindings then]
     (if-let-helper* bindings then nil))
  ([bindings then else]
     (if-let-helper* bindings then else)))

@pingles
Copy link
Author

pingles commented Mar 14, 2013

Haha, nice... I've updated my code above- fixes the test evaluation at expansion rather than run time. Only thing to fix is the duplicate evaluation of the tests.

@ragnard
Copy link

ragnard commented Mar 14, 2013

Cool!

One thing to consider might be to use and instead of (every? identity tsts#), otherwise all tests will always be executed, but I guess that depends on what you're expecting if-let* to do. Interesting that there are so many subtle things about a macro like this.

@pingles
Copy link
Author

pingles commented Mar 14, 2013

Cool- the gist now uses and. Final step is to avoid duplicate evaluation of the test expressions. I wondered whether a sequence that had already been realised wouldn't require the re-evaluation of the tests but it doesn't :(

(defmacro if-let*
  ([bindings then]
     `(if-let* ~bindings ~then nil))
  ([bindings then else & oldform]
     (let [testexprs (map last (partition 2 bindings))
           names (map first (partition 2 bindings))
           test (cons #'and testexprs)]
       `(if ~test
          (let ~(vec (interleave names testexprs))
            ~then)
          ~else))))

(comment
  (if-let* [a (do (println "a") 1)
            b (do (println "b") 3)]
           (+ a b))
  ;; a
  ;; b
  ;; a
  ;; b
  ;; => 4

Any thoughts?

@ragnard
Copy link

ragnard commented Mar 14, 2013

Maybe my suggestion to use and was actually a bad idea since we "lose" the values of resulting from the tests... I'm starting to think the nested if-let approach might actually be necessary, at least if:

  1. Tests should be evaluated lazily, ie. first failure should short-circuit
  2. Tests should only be evaluated once

But there is most probably other possibilities/solutions too...

@alexdowad
Copy link

Just use recursion!

(defmacro if-let*
  ([bindings then]
     `(if-let* ~bindings ~then nil))
  ([bindings then else]
    (if (empty? bindings)
      then
      `(if-let ~(vec (take 2 bindings))
        (if-let* ~(drop 2 bindings) ~then ~else)
        ~else))))

This way you don't evaluate any of the test expressions more than once, and you don't lose their values either.

@ertugrulcetin
Copy link

ertugrulcetin commented Jul 26, 2016

Here is the new if-let* imp:

(defmacro if-let*
  ([bindings then]
   `(if-let* ~bindings ~then nil))
  ([bindings then else]
   (if (seq bindings)
     `(if-let [~(first bindings) ~(second bindings)]
        (if-let* ~(drop 2 bindings) ~then ~else)
        ~(if-not (second bindings) else))
     then)))
(if-let* [a 1
           b (+ a 1) ]
           b)
;;=> 2
(if-let* [a 1
           b (+ a 1)
           c  false] ;;false or nil - does not matter
           b
           a)

;;=> 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment