user2540914 user2540914 - 1 year ago 72
HTTP Question

How do I map over a list of async channels in the order they exist in a list?

I'm having trouble returning the values from core.async channels in the browser in the order they were created (as opposed to the order at which they return a value). The channels themselves are returned from mapping cljs-http.client/get over a list of URLs.

If I bind the results manually in a

block then I can return the results in the order of the channels "by hand", but this obviously a problem when I don't know how many channels exist.

(let [response-channels (map #(http/get "" {:with-credentials? false}) (range 3))]

; Response is now three channels generated by http/get:

; #object[cljs.core.async.impl.channels.ManyToManyChannel]
; #object[cljs.core.async.impl.channels.ManyToManyChannel])

; If I want the results back in the guaranteed order that I made them, I can do this:
(go (let [response1 (<! (nth response-channels 0))
response2 (<! (nth response-channels 1))
response3 (<! (nth response-channels 2))]
(println "This works as expected:" response1 response2 response3))))

But if I try to map
over the channels instead of binding to them individually then I just get a the list of channels instead of their values.

(let [response-channels (map #(http/get "" {:with-credentials? false}) (range 3))]

(let [responses (into [] (map (fn [c] (go (<! c))) response-channels))]
(println "This just returns the channels:" responses)

; This is still just a vec of many-to-many channels
; [#object[cljs.core.async.impl.channels.ManyToManyChannel]
; #object[cljs.core.async.impl.channels.ManyToManyChannel]
; #object[cljs.core.async.impl.channels.ManyToManyChannel]]

I suspect it's a problem with the location of the
block, however I can't move it outside of the anonymous function without an error that I'm using
outside of a

This doesn't work:

(into [] (go (map <! response-channels)))

And neither does this:

(go (let [responses (into [] (map <! response-channels))]))

I also tried merging the channels via
and then using
to conjoin the values but results are in the order of when the requests were fulfilled, not the order of the channels being merged.

Can anyone shed some light on retrieving values from a list of channels in the order the channels exist in the list?

Answer Source

In Clojure you could do (map <!! response-channels), but that's not possible in ClojureScript. What's even more important is that it's discouraged to use map—or lazy operations in general—for side effects (checkout this blog post to see why). The reason your code doesn't yield the results you're expecting is the (nested) use of fn within the go block (see this answer):

By [the Clojure go-block] stops translation at function boundaries, I mean this: the go block takes its body and translates it into a state-machine. Each call to <! >! or alts! (and a few others) are considered state machine transitions where the execution of the block can pause. At each of those points the machine is turned into a callback and attached to the channel. When this macro reaches a fn form it stops translating. So you can only make calls to <! from inside a go block, not inside a function inside a code block.

I'm not quite sure, but when you have a look at (source map) you'll see that it invokes fn directely as well as via other functions (such as lazy-seq), which is probably why (go (map <! response-channels)) doesn't work.

Anyway, how about doseq:

(go (doseq [c response-channels]
      (println (<! c))))

This will respect the order within response-channels.