The most versatile way to do this is a pattern, I like to call concatenated lists of strings.
You implement the parts as functions returning sequences of strings.
You combine parts with concat. And map parts over input data with mapcat.
Interpose and interleave have the role of str/join.
At the very top level, you can either (apply str (toplevel-part data)) or write the string parts directly to a socket, like ring does.
In clojure, this is already pretty efficient, thanks to lazy sequences.
(do (time
(->> data-struct
(map #(interpose ", " (vals %)))
(interpose [\newline])
(apply concat)
(apply str)))
nil)
; => "Elapsed time: 1104.311277 msecs"
If you still want to save on the allocation of the lazy sequence, you could write it in reducer style with a StringBuilder as state and .append as a step function.
(defn interpose-xf
"A transducer version of interpose"
[sep]
(fn [xf]
(let [is-first (volatile! true)]
(completing
(fn [s v]
(-> (if @is-first
(do (vreset! is-first false) s)
(xf s sep))
(xf v)))))))
(defn sb-append [sb s] (.append ^StringBuilder sb (str s)))
(do (time (str (transduce
(comp (map #(eduction (comp (map val) (interpose-xf ", ")) %))
(interpose-xf [\newline])
cat)
sb-append
(StringBuilder.) data-struct)))
nil)
; => "Elapsed time: 578.444552 msecs"