关于clojure:Core.async:从promise-chans集合中获取所有值

Core.async: Take all values from collection of promise-chans

考虑这样的数据集:

1
2
3
4
(def data [{:url"http://www.url1.com" :type :a}
           {:url"http://www.url2.com" :type :a}
           {:url"http://www.url3.com" :type :a}
           {:url"http://www.url4.com" :type :b}])

这些URL的内容应并行请求。根据项目的:type值,这些内容应由相应的函数解析。一旦所有响应到达,解析函数将返回集合,该集合应该被串联。

因此,我们假设存在函数parse-aparse-b,它们在传递包含HTML内容的字符串时都返回字符串的集合。

看来core.async可能是一个很好的工具。每个项目可能有单独的频道,也可能只有一个频道。我不确定哪种方法更合适。通过多个通道,可以使用换能器进行后处理/解析。还有一个特殊的promise-chan可能在这里合适。

这是一个代码草图,我正在使用基于回调的HTTP kit函数。不幸的是,我在go块内找不到通用的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
(defn f [data]
  (let [chans (map (fn [{:keys [url type]}]
                     (let [c (promise-chan (map ({:a parse-a :b parse-b} type)))]
                       (http/get url {} #(put! c %))
                       c))
                   data)
        result-c (promise-chan)]
    (go (put! result-c (concat (<! (nth chans 0))
                               (<! (nth chans 1))
                               (<! (nth chans 2))
                               (<! (nth chans 3)))))
    result-c))

结果可以这样读取:

1
(go (prn (<! (f data))))


我想说promise-chan弊大于利。问题在于大多数core.async API(a/mergea/reduce等)依赖于以下事实:通道将在某个点关闭,而promise-chan则再也不会关闭。

因此,如果坚持使用core.async对于您至关重要,则更好的解决方案是不使用promise-chan,而是使用普通通道,该通道将在第一个put!之后关闭:

1
2
3
4
5
...
(let [c (chan 1 (map ({:a parse-a :b parse-b} type)))]
  (http/get url {} #(do (put! c %) (close! c)))
  c)
...

至此,您正在使用封闭通道,事情变得更简单了。要收集所有值,您可以执行以下操作:

1
2
3
4
5
6
7
8
;; (go (put! result-c (concat (<! (nth chans 0))
;;                            (<! (nth chans 1))
;;                            (<! (nth chans 2))
;;                            (<! (nth chans 3)))))
;; instead of above, now you can do this:
(->> chans
     async/merge
     (async/reduce into []))

UPD(以下是我的个人意见):

似乎,使用core.async通道作为承诺(以promise-chan的形式或在单个put!之后关闭的通道)不是最佳方法。当事情发展时,事实证明core.async API总体上(您可能已经注意到)并不那么令人愉快。另外,还有一些不受支持的构造,这可能迫使您编写的惯用代码要少得多。此外,没有内置的错误处理功能(如果在go -block内发生错误,go -block会静默返回nil),并且要解决此问题,您需要自己解决一些问题(重新发明轮子)。因此,如果您需要承诺,我建议为此使用特定的库,例如manifoldpromesa


如果仍然有人在看这个,请添加@OlegTheCat的答案:

您可以使用单独的渠道处理错误。

1
2
3
4
5
6
7
8
9
10
11
12
(:require [cljs.core.async :as async]
            [cljs-http.client :as http])
(:require-macros [cljs.core.async.macros :refer [go]])

(go (as-> [(http/post <url1> <params1>)
           (http/post <url2> <params2>)
           ...]
          chans
          (async/merge chans (count chans))
          (async/reduce conj [] chans)
          (async/<! chans)
          (<callback> chans)))

我也需要此功能,因为我真的很喜欢core.async,但是我也想在某些地方使用它,例如传统的JavaScript promises。我想出了使用宏的解决方案。在下面的代码中,是相同的东西,但是如果有错误,它将抛出。它的行为就像Promise.all()一样,如果它们都成功返回了所有通道返回值的vector;否则它将返回第一个错误(因为将导致它抛出该值)。

1
2
3
4
5
(defmacro <<? [chans]
  `(let [res# (atom [])]
     (doseq [c# ~chans]
       (swap! res# conj (serverless.core.async/<? c#)))
     @res#))

如果您想查看该功能的完整上下文,请访问GitHub。灵感来自David Nolen的博客。


async.core中使用pipeline-async并发启动诸如http/get之类的异步操作,同时以与输入相同的顺序传递结果:

1
2
3
4
5
6
7
8
9
(let [result (chan)]
  (pipeline-async
    20 result
    (fn [{:keys [url type]} ch]
      (let [parse ({:a parse-a :b parse-b} type)
            callback #(put! ch (parse %)(partial close! ch))]  
        (http/get url {} callback)))
    (to-chan data))
  result)