无法在core.async的go块中使用for循环?

10

我刚接触clojure core.async库,正在通过实验尝试理解它。

但当我尝试以下代码时:

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

它给我一个非常奇怪的异常:

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

然后我尝试了另一个代码:

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

完全没有编译器异常。

我完全困惑了。发生了什么?


1
在Clojure中,“for”不是循环 - 它是列表推导式。有了这个想法,您的“for”示例不是调用具有副作用的>!函数的惯用方式。也许编译器消息可以/应该改进,但您的根本问题是以这种方式使用“for”没有(clojure)意义。“doseq”完全没问题。 - sw1nn
当程序遇到"put!"块时,它会等待有人从通道读取数据。请先设置读取部分,或者使用"put!"来实现。 - edbond
@edbond 哇,这真的有效..但我更加困惑了,Timothy Baldridge不是说async/go不能处理async/go块中的fn吗? - xudifsd
1个回答

19

因此,Clojure的go-block在函数边界处停止翻译,有很多原因,但最大的原因是简单性。这在构建惰性序列时最常见:

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

被编译成类似于这样的东西:

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

现在让我们快速考虑一下...这应该返回什么?假设您想要的是一个包含从c中取出的值的lazy seq,但<!需要将函数的其余代码转换为回调,但LazySeq希望函数是同步的。实际上没有绕过这种限制的方法。

所以回到你的问题,如果您使用macroexpand for,您会发现它实际上并没有循环,而是扩展成一堆最终调用lazy-seq的代码,因此停车操作不起作用于体内。然而,doseq(和dotimes)由loop/recur支持,因此可以完美地工作。

还有其他几个地方可能会让您困扰,例如with-bindings。基本上,如果宏将您的core.async停车操作插入嵌套函数中,您将收到此错误。

那么我的建议是尽可能简化go块的主体。编写纯函数,然后将go块的主体视为执行IO的地方。

------------编辑-----------------

通过在函数边界处停止翻译,我是指:go块将其主体转换为状态机。对<! >!alts!(以及其他一些)的每次调用都被视为状态机转换,其中块的执行可以暂停。在这些点中的每一个,机器都会被转化为回调并附加到通道上。当此宏达到fn形式时,它会停止翻译。因此,您只能从go块内部而不是在代码块内部的函数中调用<!

这是core.async的魔力之一。没有go宏,core.async代码看起来很像其他语言中的回调地狱。


1
请问您能解释一下在函数边界处停止翻译是什么意思吗? - Chiron
1
添加到我的原始帖子中。 - Timothy Baldridge
我明白了,所以也许core.async可以在:fn中添加一个方法,并抛出更好的异常。这对初学者来说非常不友好。 - xudifsd
从评论中学到了,(let [i (async/chan)] (async/go (doall (for [r [1 2 3]] (async/put! i r)))) (async/go (println (async/<! i)))) 实际上是可行的,这不与 fn 不能出现在 async/go 块的想法相冲突吗? - xudifsd
这是否意味着如果一个库(data.xml)需要一个序列,你就完蛋了? - Cramer
@xudifsd 这仅适用于<!,>!,alt!和alts!。您可以在go块之外使用put!。因此,如果文档字符串没有说明必须在go块内使用它,则可以在任何地方使用它,包括嵌套的fn中。 - Didier A.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接