couldn't use for loop in go block of core.async?
Asked Answered
S

1

10

I'm new to clojure core.async library, and I'm trying to understand it through experiment.

But when I tried:

(let [i (async/chan)] (async/go (doall (for [r [1 2 3]] (async/>! i r)))))

it gives me a very strange exception:

CompilerException java.lang.IllegalArgumentException: No method in multimethod '-item-to-ssa' for dispatch value: :fn

and I tried another code:

(let [i (async/chan)] (async/go (doseq [r [1 2 3]] (async/>! i r))))

it have no compiler exception at all.

I'm totally confused. What happend?

Safire answered 25/9, 2014 at 14:17 Comment(3)
for in clojure is not a loop - it's a list comprehension. With this in mind your for example is not an idiomatic way to invoke the side effecting >! function. Maybe the compiler message could/should be improved, but your fundamental problem is that using for in this way doesn't make (clojure) sense. The doseq is perfectly fine.Incise
>! blocks, it waits for someone to read from channel. Try to setup reading part first, or use put!Oxa
@Oxa wow, this actually works.. but I'm even more confused, didn't Timothy Baldridge just said async/go couldn't handle fn in async/go block?Safire
S
19

So the Clojure go-block stops translation at function boundaries, for many reasons, but the biggest is simplicity. This is most commonly seen when constructing a lazy seq:

(go (lazy-seq (<! c)))

Gets compiled into something like this:

(go (clojure.lang.LazySeq. (fn [] (<! c))))

Now let's think about this real quick...what should this return? Assuming what you probably wanted was a lazy seq containing the value taken from c, but the <! needs to translate the remaining code of the function into a callback, but LazySeq is expecting the function to be synchronous. There really isn't a way around this limitation.

So back to your question if, you macroexpand for you'll see that it doesn't actually loop, instead it expands into a bunch of code that eventually calls lazy-seq and so parking ops don't work inside the body. doseq (and dotimes) however are backed by loop/recur and so those will work perfectly fine.

There are a few other places where this might trip you up with-bindings being one example. Basically if a macro sticks your core.async parking operations into a nested function, you'll get this error.

My suggestion then is to keep the body of your go blocks as simple as possible. Write pure functions, and then treat the body of go blocks as the places to do IO.

------------ EDIT -------------

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

This is part of the magic of core.async. Without the go macro, core.async code would look a lot like callback-hell in other langauges.

Sludgy answered 25/9, 2014 at 15:17 Comment(6)
Would you please explain what do you mean by stops translation at function boundaries ?Jaquez
added to my original postSludgy
I see, so maybe core.async could add one more method to :fn and throw a better exception. It's very beginner unfriendly.Safire
learnt from comment, and (let [i (async/chan)] (async/go (doall (for [r [1 2 3]] (async/put! i r)))) (async/go (println (async/<! i)))) actually works, isn't this conflict with the idea that fn couldn't appear in async/go block?Safire
Does this mean that if a library (data.xml) wants a sequence you're SOL?Cassandry
@Safire This only applies too <!, >!, alt! and alts!. You can use put! outside of a go block. So if the doc-string doesn't say that it must be used inside of a go block, than you can use it anywhere, including in a nested fn.Chondrite

© 2022 - 2024 — McMap. All rights reserved.