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
> 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