the simplest (but not the most FP-ish) is almost identical to your example:
(let [v [1 2 3 4 5 6 7]] (doseq [i (range (count v)) j (range (inc i) (count v))] (println (vi) (vj))))
and here is a more functional option for generating all these pairs (it does not depend on the length or indices, but rather on the tail iteration):
(let [v [1 2 3 4 5 6 7]] (mapcat #(map (partial vector (first %)) (rest %)) (take-while not-empty (iterate rest v))))
output:
([1 2] [1 3] [1 4] [1 5] [1 6] [1 7] [2 3] [2 4] [2 5] [2 6] [2 7] [3 4] [3 5] [3 6] [3 7] [4 5] [4 6] [4 7] [5 6] [5 7] [6 7])
then just use these pairs in doseq for any side effect:
(let [v [1 2 3 4 5 6 7] pairs (fn [items-seq] (mapcat #(map (partial vector (first %)) (rest %)) (take-while not-empty (iterate rest items-seq))))] (doseq [[i1 i2] (pairs v)] (println i1 i2)))
update: after @ dg123 answer. this is nice, but you can do it even better by using doseq and for functions such as destructuring and protection:
(let [v [1 2 3 4 5 6 7]] (doseq [[x & xs] (iterate rest v) :while xs y xs] (println "x:" x "y:" y)))
you iterate over the tails of the collection, but remember that iterate creates endless code:
user> (take 10 (iterate rest [1 2 3 4 5 6 7])) ([1 2 3 4 5 6 7] (2 3 4 5 6 7) (3 4 5 6 7) (4 5 6 7) (5 6 7) (6 7) (7) () () ())
so you need to somehow limit it to including only empty collections. the destructive form [x & xs] divides the argument into the first pair and the sequence of other parameters:
user> (let [[x & xs] [1 2 3 4 5 6]] (println x xs)) 1 (2 3 4 5 6) nil
and when the linked collection is empty or has one element, xs will be nil :
user> (let [[x & xs] [1]] (println x xs)) 1 nil nil
so you just use this function using :while guard in list comprehension.
in the end you just create pairs (or do some side effects in this case) for x and each element in xs