let-while

574 views
Skip to first unread message

mbrodersen

unread,
Oct 16, 2009, 7:58:55 PM10/16/09
to Clojure
Hi,

I am new to Clojure and I am wondering if there is anything similar to
the following macro already built in:

(defmacro let-while
"Makes it easy to continue processing an expression as long as it is
true"
[[name expr] & forms]
`(loop []
(let [~name ~expr]
(when ~name
~@forms
(recur)))))

I find it really useful when (for example) reading from a non-blocking
socket. Here is a quick (silly) example:

(let-while [x (if (> (rand) 0.2) "Yep!" nil)]
(println x)
(println (str x)))

The idea is that x is bound to the if expression and if x is true the
println's are executed. Repeat until x is false/nil.

Thanks
Morten

John Harrop

unread,
Oct 16, 2009, 8:12:06 PM10/16/09
to clo...@googlegroups.com
On Fri, Oct 16, 2009 at 7:58 PM, mbrodersen <morten.b...@gmail.com> wrote:

Hi,

I am new to Clojure and I am wondering if there is anything similar to
the following macro already built in:

(defmacro let-while
       "Makes it easy to continue processing an expression as long as it is
true"
       [[name expr] & forms]
       `(loop []
               (let [~name ~expr]
                       (when ~name
                               ~@forms
                               (recur)))))

Nice.

But name is multiply evaluated. This might be preferable:

(defmacro let-while
  "Makes it easy to continue processing an expression as long as it is true"
  [[name expr] & forms]
  (let [n# ~name]
    `(loop []
       (let [~n# ~expr]
         (when ~n#
           ~@forms
           (recur)))))
 
This evaluates the name argument first, and only once, and (as required) evaluates expr and forms once each time around the loop, plus expr one additional time (the first time it evaluates to false).

John Harrop

unread,
Oct 16, 2009, 8:13:09 PM10/16/09
to clo...@googlegroups.com
On Fri, Oct 16, 2009 at 8:12 PM, John Harrop <jharr...@gmail.com> wrote:
(defmacro let-while
  "Makes it easy to continue processing an expression as long as it is true"
  [[name expr] & forms]
  (let [n# ~name]
    `(loop []
       (let [~n# ~expr]
         (when ~n#
           ~@forms
           (recur)))))

Dammit.

(defmacro let-while
  "Makes it easy to continue processing an expression as long as it is true"
  [[name expr] & forms]
  `(let [n# ~name]
     (loop []
       (let [n# ~expr]
         (when n#
           ~@forms
           (recur))))) 

mbrodersen

unread,
Oct 17, 2009, 3:14:50 AM10/17/09
to Clojure
I think I understand what you are trying to do John but it doesn't
have the same semantics though.

If you use the first definition, the following expression:

(let-while [x (if (> (rand) 0.2) "Yep!" nil)] (println x) (println
(str x))))

will work correctly (it will print "Yep!" and "Yep!Yep!" zero or more
times when called).

If you use the 2nd macro definition, the x value will not be bound to
the result of the (if ...) expression => (println x) will trigger a
"Unable to resolve symbol: x" error.

Timothy Pratley

unread,
Oct 17, 2009, 3:32:20 AM10/17/09
to Clojure
> But name is multiply evaluated. This might be preferable:

Hi John,

Could you explain this a bit more for me? I can understand if the
condition is duplicated that is unnecessary calculation but don't
appreciate the issue with the binding name... both versions of the
macro seem to expand to identical code:
user=> (macroexpand-1 '(while-let [a nil] (foo)))
(clojure.core/loop [] (clojure.core/let [a nil] (clojure.core/when a
(foo) (recur))))
So my naive view was they would have identical behaviour/performance.
I've heard this come up before but never understood or been able to
find any reference about this 'issue'.


Regards,
Tim.

Jarkko Oranen

unread,
Oct 17, 2009, 6:36:23 AM10/17/09
to Clojure
I don't think the multiple evaluation matters in this case, since it's
the name parameter, which will in all sane cases be a simple symbol.
gensyms are needed when multiple evaluation could cause side-effects
or an expensive function to be computed multiple times, which is not
the case here.

mbrodersen

unread,
Oct 17, 2009, 6:47:19 AM10/17/09
to Clojure
> I don't think the multiple evaluation matters in this case, since it's
> the name parameter, which will in all sane cases be a simple symbol.
> gensyms are needed when multiple evaluation could cause side-effects
> or an expensive function to be computed multiple times, which is not
> the case here.

Yes that is correct Jarkko. The name parameter will only be evaluated
as needed and it serves the same purpose as x in the following:

(doseq [x [1 2 3]] (println x))

John Harrop

unread,
Oct 17, 2009, 12:29:27 PM10/17/09
to clo...@googlegroups.com
In this case, most often the name parameter will just be a symbol, but it's generally a good idea to avoid reoccurrences of the same macro argument in the macro expansion, except when it's needed. In this case the test and body expression *should* be multiply evaluated for the semantics of a looping construct to occur, though.

Christophe Grand

unread,
Oct 17, 2009, 2:27:27 PM10/17/09
to clo...@googlegroups.com
Hi all,

On Sat, Oct 17, 2009 at 2:12 AM, John Harrop <jharr...@gmail.com> wrote:
On Fri, Oct 16, 2009 at 7:58 PM, mbrodersen <morten.b...@gmail.com> wrote:

Hi,

I am new to Clojure and I am wondering if there is anything similar to
the following macro already built in:

(defmacro let-while
       "Makes it easy to continue processing an expression as long as it is
true"
       [[name expr] & forms]
       `(loop []
               (let [~name ~expr]
                       (when ~name
                               ~@forms
                               (recur)))))

Nice.

But name is multiply evaluated.


It's a valid point: since name is used in test-position in the chen clause we must ensure that's it's a symbol and not a destructuring form (map or vector).
eg
  (let-while [[a b] nil] (println "you should never see this message"))

But the proposed version doesn't solve anything.

Here is a better one:
(defmacro while-let

 "Makes it easy to continue processing an expression as long as it is true"
 [binding & forms]
  `(loop []
     (when-let ~binding
       ~@forms
       (recur))))

(I cheated: rather than manually working around this bug, I used when-let which already does the right thing.)

hth,

Christophe


--
Professional: http://cgrand.net/ (fr)
On Clojure: http://clj-me.blogspot.com/ (en)

mbrodersen

unread,
Oct 17, 2009, 10:28:58 PM10/17/09
to Clojure
That's a nice improvement Christophe. Exactly the kind of answer I was
looking for.

Thanks!
Morten

Timothy Pratley

unread,
Oct 17, 2009, 11:11:29 PM10/17/09
to Clojure


On Oct 18, 5:27 am, Christophe Grand <christo...@cgrand.net> wrote:
> (defmacro while-let
>  "Makes it easy to continue processing an expression as long as it is true"
>  [binding & forms]
>   `(loop []
>      (when-let ~binding
>        ~@forms
>        (recur))))

Nice! Would you be willing to add it to contrib? I've found while-let
a very useful form and this version is really neat.

Where should it belong I wonder... the most related thing I could see
in contrib was:
http://richhickey.github.com/clojure-contrib/cond-api.html#cond/cond-let
Maybe cond-let and while-let should fall under 'let' instead of
'cond', or 'core' seeing they are slight extensions of core functions.


Regards,
Tim.

John Harrop

unread,
Oct 17, 2009, 11:30:20 PM10/17/09
to clo...@googlegroups.com
On Sat, Oct 17, 2009 at 2:27 PM, Christophe Grand <chris...@cgrand.net> wrote:
(defmacro while-let

 "Makes it easy to continue processing an expression as long as it is true"
 [binding & forms]
  `(loop []
     (when-let ~binding
       ~@forms
       (recur))))

This does look like the best implementation so far.

Maybe a general tip for macro design: if you want one or more bindings, see if you can't make the macro simply stick these into an existing binding-using form, like let, loop, if-let, etc.

(I cheated: rather than manually working around this bug, I used when-let which already does the right thing.)

It's not cheating to build on existing, useful library functionality. And when you can, it's silly not to. I just wish I'd thought of it first.

mbrodersen

unread,
Oct 17, 2009, 11:42:28 PM10/17/09
to Clojure
It would be great to have while-let in contrib. Then I don't have to
maintain let-while myself :-)

Morten
Reply all
Reply to author
Forward
0 new messages