doseq vs. map

2,190 views
Skip to first unread message

Sean

unread,
Apr 2, 2009, 10:06:44 PM4/2/09
to Clojure
Hi everyone,
I'm working with awt to do create an image renderer. This is
obviously an application where side effects are desired. My first
attempt was this:

(map #(form-with-side-effects %) a-list)

This didn't do what I expected. After a little while, I found the
doseq macro. I re-wrote my code like this:

(doseq [atom a-list]
(form-with-side-effects atom))

And now everything works great. What I don't understand is why the
doseq macro is required instead of the mapping operation. Could
somebody explain why Clojure has this different form for functions
that have side effects?

Sean

Daniel Jomphe

unread,
Apr 2, 2009, 10:17:40 PM4/2/09
to Clojure
From map's docstring: "Returns a lazy sequence [...]"

So I guess you applied map at the top level and wondered why side-
effects didn't happen.

Try:

(dorun (map #(form-with-side-effects %) a-list))

Or, for fun:

(take 1 (map #(form-with-side-effects %))

Basically, since your map wasn't needed, it wasn't "realized"/
executed. Laziness.

Daniel Jomphe

unread,
Apr 2, 2009, 10:20:12 PM4/2/09
to Clojure
Daniel Jomphe wrote:
> Basically, since your map wasn't needed, it wasn't "realized"/
> executed. Laziness.

Better said:

Basically, since your map's results weren't used, it wasn't
"realized"/
evaluated. That's why you didn't see your expected side effects.
Laziness.

Matt Revelle

unread,
Apr 2, 2009, 10:29:09 PM4/2/09
to Clojure
Were you in #clojure earlier? This came up there and pjstadig and I
raced to implement "domap" and then slashus2 pointed out there was no
need for it to be a macro.

http://gist.github.com/89249

(defn domap
"A map for side-effects. The argument order is the same as map, but
unlike map the function results are not retained. Takes a function
followed by any number of collections and applies the function to the
first item in each coll, then the second etc. Returns nil."
[fn & colls]
(let [num-colls (count colls)]
(doseq [args (partition num-colls (apply interleave colls))]
(apply fn args))))

Sean

unread,
Apr 2, 2009, 10:34:35 PM4/2/09
to Clojure
Thanks for the response everyone! I was able to get it working. If I
understand what everyone is saying, the following statement is true:

In Clojure, laziness is the rule not the exception.

Eric Tschetter

unread,
Apr 3, 2009, 3:29:44 AM4/3/09
to clo...@googlegroups.com
> Thanks for the response everyone!  I was able to get it working.  If I
> understand what everyone is saying, the following statement is true:
>
> In Clojure, laziness is the rule not the exception.

Rather than that, I'd say that you are thinking about map wrong (hope
that doesn't come across as abrasive).

"Map" in its purest sense says "take a collection of X, apply this
function to each element of it to produce something of type Y and then
give me the resulting collection". That is, "map" is more geared
towards type "conversion" of the elements in a collection, but it is
only converting the elements in the collection, not the collection
itself.

What you are looking for is something more like reduce. Reduce (also
known as fold left) consumes a collection and turns it into something
else. For example, you can implement doseq in terms of reduce as
follows.

(defn permutations
"creates a sequence of all permutations of multiple sequences"
[curr-seq & rest-seqs]
(if (nil? rest-seqs)
(map #(cons % '()) curr-seq)
(reduce concat (map (fn [curr-seq-val] (map #(cons curr-seq-val %)
(apply permutations rest-seqs))) curr-seq))))

user=> (permutations '(1 2) '(a b c d))
((1 a) (1 b) (1 c) (1 d) (2 a) (2 b) (2 c) (2 d))
user=> (permutations '(1) '(a b) '(I II III))
((1 a I) (1 a II) (1 a III) (1 b I) (1 b II) (1 b III))

(defmacro mydoseq
"Uses reduce to implement doseq"
[forms & body]
(let [form-pairings (partition 2 forms)
vars (vec (map #(first %) form-pairings))
assignments (map #(second %) form-pairings)]
`(reduce (fn ~'[nothing args] (apply (fn ~vars ~@body) ~'args))
nil
(permutations ~@assignments))))

user=> (doseq [a '(3 4 5 6)] (println (+ a 2)))
5
6
7
8
nil
user=> (mydoseq [a '(3 4 5 6)] (println (+ a 2)))
5
6
7
8
nil

user=> (doseq [a '(3 4 5) b '(5 4 3)] (println (+ a b)))
8
7
6
9
8
7
10
9
8
nil
user=> (mydoseq [a '(3 4 5) b '(5 4 3)] (println (+ a b)))
8
7
6
9
8
7
10
9
8
nil

user=> (macroexpand '(mydoseq [a '(3 4 5) b '(5 4 3)] (println (+ a b))))
(clojure.core/reduce (clojure.core/fn [nothing args]
(clojure.core/apply (clojure.core/fn [a b] (println (+ a b))) args))
nil (user/permutations (quote (3 4 5)) (quote (5 4 3))))

Which becomes

(reduce
(fn [nothing args] (apply (fn [a b] (println (+ a b))) args))
nil
(permutations '(3 4 5) '(5 4 3))))


Or, you can give a little bit different semantics:

(defn combine
"combines multiple sequences into a sequence of the collection of
their nth elements"
[& colls]
(when (not (some nil? colls))
(lazy-cons
(map #(first %) colls)
(apply combine (map #(rest %) colls)))))

user=> (combine '(1 2) '(a b c d))
((1 a) (2 b))
user=> (combine '(1 2 3 4) '(a b c d))
((1 a) (2 b) (3 c) (4 d))
user=> (combine '(1 2 3 4) '(a b))
((1 a) (2 b))


(defmacro mydoseq2
"Uses reduce to implement doseq"
[forms & body]
(let [form-pairings (partition 2 forms)
vars (vec (map #(first %) form-pairings))
assignments (map #(second %) form-pairings)]
`(reduce (fn ~'[nothing args] (apply (fn ~vars ~@body) ~'args))
nil
(combine ~@assignments))))


user=> (doseq [a '(3 4 5 6)] (println (+ a 2)))
5
6
7
8
nil
user=> (mydoseq2 [a '(3 4 5 6)] (println (+ a 2)))
5
6
7
8
nil

user=> (doseq [a '(3 4 5 6) b '(6 5 4 3)] (println (+ a b)))
9
8
7
6
10
9
8
7
11
10
9
8
12
11
10
9
nil
user=> (mydoseq2 [a '(3 4 5 6) b '(6 5 4 3)] (println (+ a b)))
9
9
9
9
nil

André Ferreira

unread,
Apr 2, 2009, 11:56:54 PM4/2/09
to Clojure
Laziness is the rule only on sequence operations.

wlr

unread,
Apr 3, 2009, 10:45:07 AM4/3/09
to Clojure
On Apr 3, 3:29 am, Eric Tschetter <eched...@gmail.com> wrote:
> That is, "map" is more geared towards type "conversion" of the
> elements in a collection, but it is only converting the elements in
> the collection, not the collection itself.
>
> (some ellided text here...)
>
> Reduce (also known as fold left) consumes a collection and turns
> it into something else.
>

Thank you for this "higher level" perspective. Sometimes I (we?) get
caught up in the details - the old "forest vs trees" problem.

Phil Hagelberg

unread,
Apr 3, 2009, 2:43:46 PM4/3/09
to clo...@googlegroups.com
Sean <francoi...@gmail.com> writes:

> And now everything works great. What I don't understand is why the
> doseq macro is required instead of the mapping operation. Could
> somebody explain why Clojure has this different form for functions
> that have side effects?

My take on this is that map supports a functional style of programming,
(it takes a function argument, and the focus is on its return value)
while doseq is meant for imperative, side-effect-producing code. Both
are possible in Clojure, but you should use FP as much as possible as it
will result in cleaner, less-buggy code.

If doseq and map worked the same way, you would be tempted to use them
interchangably. This would be a mistake as they are each suited for
vastly different paradigms, despite superficial similarities in the way
they work.

-Phil

Reply all
Reply to author
Forward
0 new messages