Let usage question

10 views
Skip to first unread message

Dave Ray

unread,
Oct 19, 2010, 11:19:17 PM10/19/10
to clo...@googlegroups.com
Hey,

I'm parsing a file with a chain of filter and map operations. To make
it a little more readable (to me), I put the steps in a let like this:

(defn parse-dictionary
[reader]
(let [lines (read-lines reader)
trimmed (map #(.trim %1) lines)
filtered (filter is-dictionary-entry? trimmed)]
(map parse-entry filtered)))

Is this style of let considered good/bad stylistically? Are there
technical tradeoffs between this and a bunch of nested forms?

Thanks!

Dave

Stuart Campbell

unread,
Oct 20, 2010, 12:27:57 AM10/20/10
to clo...@googlegroups.com

Not sure about the technical implications, but I can offer another alternative:

(defn parse-dictionary
  [reader]
  (->> (read-lines reader)
       (map #(.trim %1))
       (filter is-dictionary-entry?)
       (map parse-entry)))

I think this reads about as well as the (let ...) version. It's easy enough to trace the 'flow' of execution through the various forms.

Regards,
Stuart

Tom Faulhaber

unread,
Oct 20, 2010, 2:41:46 AM10/20/10
to Clojure
Dave,

Yes, this is perfectly idiomatic and many people in Clojure (and also
Haskell, for example) use let to help document how they're building up
their computation.

Stuart's suggestion is also good and it's largely a matter of personal
preference which to use when.

Of course, as you use clojure more, you'll probably become more
comfortable with more complex statements and not use the let style
quite so much.

Tom

Alan

unread,
Oct 20, 2010, 12:52:11 PM10/20/10
to Clojure
I agree with Tom (and with Stuart). I tend to like using ->> when it's
convenient, since all you're really doing is performing a list of
transformations on a single object. However, the let is better
documentation if that's ever going to matter. Not because it makes it
easier to understand what operations are being performed - ->> is just
as good at that - but because you assign names to the intermediate
results. Then someone reading your code can see what the purpose of
each transformation is, without having to look at the definition of
other functions.

lprefo...@softaddicts.ca

unread,
Oct 20, 2010, 1:20:08 PM10/20/10
to clo...@googlegroups.com
Hi,

readability might be a concern but it's not the only criteria.

a) Do you need to trace intermediate results ? Then you need a
binding so you do not redo the work twice (presumably, I exclude memoized
functions here). Of course if the code has some side effects, the choice
is obvious, you do not want the tracing to redo the side effects twice...

b) Do you need to reference intermediate results in a closure ?
Then you need local bindings of course.

c) If a huge form becomes difficult to read then using local bindings will
enhance readability.

a) & b) might not be immediately obvious choices while writing the code.
They may arise later as you test/debug the code.

You may create bindings first to reflect logical chunks and then remove
them later if not needed.

I find it easier to add local bindings and removed when the code is stable
than vice-versa (but that could be an effect of aging, my brains maybe
getting slower along the way to terminal city :)))

Luc P.

Alan <al...@malloys.org> wrote ..

> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@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+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en

Alan

unread,
Oct 20, 2010, 1:28:39 PM10/20/10
to Clojure
By the way, http://tinyurl.com/2a235cn is an example of your style of
let being used in the Clojure source: the definition of defn itself.
It's a little overdone and weird-looking, but the alternative of
deeply nested forms would be much worse.

I didn't notice your question about technical tradeoffs the first
time, so I did a quick unscientific study:

(defmacro lets [n]
(let [name (gensym)]
`(let [~name 1
~@(interleave (repeat name)
(map (fn [_] (list '* 2 name))
(range n)))]
~name)))

(defmacro nested [n]
(if-not (pos? n)
1
`(* 2 (lets ~(dec n)))))

I tested them both a few times on input sizes of 300, and neither one
was consistently faster (see below for why). For what it's worth, the
nested form blows the stack for large N (eg 400 in my case), while the
let form doesn't. I could write nested more carefully, probably using
iterate, so that it doesn't break during macroexpansion. I thought at
first that it would have to break eventually due to too many nested *
calls, but I was quite surprised to see that these two macros expand
to nearly identical things:

(clojure.pprint/pprint (clojure.walk/macroexpand-all '(lets 5)))
(let*
[G__3255
1
G__3255
(* 2 G__3255)
G__3255
(* 2 G__3255)
G__3255
(* 2 G__3255)
G__3255
(* 2 G__3255)
G__3255
(* 2 G__3255)]
G__3255)

(clojure.pprint/pprint (clojure.walk/macroexpand-all '(nested 5)))
(clojure.core/*
2
(let*
[G__3258
1
G__3258
(* 2 G__3258)
G__3258
(* 2 G__3258)
G__3258
(* 2 G__3258)
G__3258
(* 2 G__3258)]
G__3258))

cej38

unread,
Oct 20, 2010, 1:34:19 PM10/20/10
to Clojure
This question leads into something that I read in Joy of Clojure (page
161 in the latest MEAP edition):
"If you manage to hold onto the head of a sequence somewhere within a
function, then that sequence will be prevented from being garbage
collected. The simplest way to retain the head of sequence is to bind
it to a local. This condition can occur with any type of value bind be
it to a reference type or through the usage of let or binding."

I am not sure what this means, but I think it would mean that using
let as you do, could cause garbage collection problems. Thus, it
might be better to follow the advice of Stuart and Luc.


Chad

PS. Clarification of the statement from the Joy of Clojure would be
helpful.

Alan

unread,
Oct 20, 2010, 1:52:02 PM10/20/10
to Clojure
When you work with a lazy sequence, Clojure (java really)
automatically garbage-collects elements you're done with. It can only
be certain you're done with them if you no longer have any reference
to them, direct or indirect. If you've bound the head of the sequence
to a local, then you can still access every element of the lazy
sequence, so none can be garbage collected. If instead you pass the
element through a set of operations without ever binding it to
anything, each element can be GCed immediately after you're done
processing it (though in practice they won't be until space is
needed).

Observe the difference:

user=> (nth (map #(make-array Double/TYPE (* % 10000))
(range))
100)
#<double[] [D@781046>

user=> (let [x (map #(make-array Double/TYPE (* % 10000))
(range))]
(nth x 100))
java.lang.OutOfMemoryError: Java heap space (NO_SOURCE_FILE:0)


You will probably need different numbers to reproduce these results on
your system depending on how much heap space you let clojure have, but
that's the idea.

And you make a good point about binding sequences to locals in a let.
I'm not sure, but I think you're right: if the sequences are large
enough the let-version will fail while the ->> (or nested-forms)
version will succeed.

Dave Ray

unread,
Oct 20, 2010, 2:29:16 PM10/20/10
to clo...@googlegroups.com
Not only did my question get answered, but I learned several new
things in the process.

Thanks!

Dave

Rich Hickey

unread,
Oct 20, 2010, 3:21:15 PM10/20/10
to Clojure


On Oct 20, 1:34 pm, cej38 <junkerme...@gmail.com> wrote:
> This question leads into something that I read in Joy of Clojure (page
> 161 in the latest MEAP edition):
> "If you manage to hold onto the head of a sequence somewhere within a
> function, then that sequence will be prevented from being garbage
> collected. The simplest way to retain the head of sequence is to bind
> it to a local. This condition can occur with any type of value bind be
> it to a reference type or through the usage of let or binding."
>
> I am not sure what this means, but I think it would mean that using
> let as you do, could cause garbage collection problems.  Thus, it
> might be better to follow the advice of Stuart and Luc.
>
> Chad
>
> PS. Clarification of the statement from the Joy of Clojure would be
> helpful.
>

That advice seems stale, given:

http://groups.google.com/group/clojure/msg/9b4e268b85c20cd6

let-bound locals get cleared on last use.

Rich

cej38

unread,
Oct 20, 2010, 3:47:31 PM10/20/10
to Clojure
great! good to know

Michael Ossareh

unread,
Oct 20, 2010, 7:44:22 PM10/20/10
to clo...@googlegroups.com
On Wed, Oct 20, 2010 at 09:52, Alan <al...@malloys.org> wrote:
I agree with Tom (and with Stuart). I tend to like using ->> when it's
convenient, since all you're really doing is performing a list of
transformations on a single object. However, the let is better
documentation if that's ever going to matter. Not because it makes it
easier to understand what operations are being performed - ->> is just
as good at that - but because you assign names to the intermediate
results. Then someone reading your code can see what the purpose of
each transformation is, without having to look at the definition of
other functions.


I also find that the let form permits you to drop print statements in to see what the outcome of various functions are;

(defn [x]
 (let [_ (prn x)

       x (transform-somehow x)

      _ (prn x)]

  x)) 

Chouser

unread,
Oct 28, 2010, 9:14:25 AM10/28/10
to clo...@googlegroups.com

The quoted section should perhaps be clarified with better
wording, but the example immediately following that quote still
accurately demonstrates the point we were trying to make:

(let [r (range 1e9)] [(first r) (last r)])
;=> [0 999999999]

(let [r (range 1e9)] [(last r) (first r)])
; java.lang.OutOfMemoryError: GC overhead limit exceeded

"Clojure's compiler can deduce that in the first example the
retention of `r` is no longer needed when the computation of
`(last r)` occurs and therefore aggressively clear it. However,
in the second example the head is needed later in the overall
computation and can no longer be safely cleared."

This is dramatically better than the "invisible" head-holding
that sometimes happened in earlier versions of Clojure (and was
noted in earlier versions of JoC), but is still good to be aware
of.

--Chouser
http://joyofclojure.com/

Reply all
Reply to author
Forward
0 new messages