我想利用clojurescript在浏览器中进行一些声明性的,功能性的编程 - 例如,能够进行懒惰的计算,如:
(def special-nums
(->> (iterate inc 1)
(filter #(= (mod % 100) 0))
(filter #(= (mod % 7001) 0))))
(time (doall (take 1 special-nums)))
(请注意,这个特定的计算对我来说并不是很有趣 - 它只是一些代码的例子,即使是第一个结果也会花费不明的时间返回)
这样的代码对于clojure来说很自然,但是它不适合在浏览器环境中调用,它可能会阻塞事件循环并使网页无响应。懒惰没有帮助,因为即使是第一个结果也可能需要很长时间才能返回(我机器上的时间为1500毫秒)。
在“普通”javascript中,使用命令式循环,我会将范围缩小并使用setTimeout
异步返回结果,从而限制我愿意在任何给定块中执行的工作。 (我甚至可以明确地引用时钟 - 例如“继续工作直到20ms已经过去,然后停止并安排另一个块。”
有没有一种方法可以在clojurescript中实现这一目标?显然任何事情都可以通过js互操作,但如果我对系统的斗争太过激烈,那么cljs的价值就会受到限制。
我很欣赏这里的任何建议/技巧。
我知道网络工作者,我知道将计算推送到另一个执行线程总是可行的 - 但对于这个问题,我想专注于在单个JS事件循环中工作的方法。
谢谢!
答案 0 :(得分:2)
启发这个答案的技术是trampoline - 一个返回答案的函数,或另一个返回答案或函数的函数.....(在这里插入递归的英文文本)。< / p>
我们实际上不能在这里使用clojure.core/trampoline因为它会占用JS消息循环。相反,我们可以使用类似下面的内容,这些内容会在js/setTimeout
内“反弹”。当f
返回某个函数时,我们会从setTimeout
调用它。当f
返回任何其他内容时,我们会调用(continuation result)
。
(defn continue-trampoline [continuation f & args]
(let [result-or-fn (apply f args)
is-answer (not (fn? result-or-fn))]
(if is-answer (continuation result-or-fn)
(do (js/setTimeout #(continue-trampoline continuation result-or-fn) 1)
nil))))
这使我们可以将问题划分为更小的部分,可以在更短的时间内单独解决。
以你的特殊例子为例,你可以像这样分解它:
(defn calculate-special-nums [n continuation]
(letfn [(accumulate-special-nums [accumulator partitions]
(if (empty? partitions) accumulator
(let [part (first partitions)
remaining-n (- n (count accumulator))
acc (->> part
(filter #(= (mod % 100) 0))
(filter #(= (mod % 7001) 0))
(take remaining-n)
(into accumulator))
is-complete (== n (count acc))]
(if is-complete acc
#(accumulate-special-nums acc (rest partitions))))))]
(let [nums (iterate inc 1)
partitions (partition 1000 nums)]
(continue-trampoline continuation
#(accumulate-special-nums [] partitions)))))
因此,此代码将计算10个特殊数字,并在计算所有10个时提醒它们,而不会使消息循环的其他用户挨饿。
(calculate-special-nums 10 #(js/alert %))
该技术可能会扩展到占用毫秒数。对于您的示例,我可以想象使用partition-by
而不是partition
。创建一个经过一段时间后返回true的函数。例如(partition-by has-the-time-elapsed? nums)
代替(partition 1000 nums)
。
正如你所说,即使在“正常”的javascript中,你也必须解决问题 - clojurescript可能会或可能不会成为昂贵计算的例外。使用纯函数范例进行编程的一个好处是每个部分都是可独立测试的。每个不同的输入的输出始终相同。希望clojurescript能够使分区问题变得更容易。
答案 1 :(得分:0)
在探索@ agent-j优秀的基于蹦床的方法后,我决定尝试core.async
方法进行比较。
它运行得非常好,它允许我用find-special-numbers
计算器将现有的by-chunk
函数包装成具有相当声明的语法:
(ns calculate.core
(:require
[figwheel.client :as fw]
[cljs.core.async :refer [chan put! <! ]])
(:require-macros [cljs.core.async.macros :refer [go-loop]]))
(enable-console-print!)
; this is the slow, dumb function -- designed to
; take a limited-size input and produce 0 or more
; results
(defn find-special-numbers [input]
(->> input
(filter #(= (mod % 100) 0))
(filter #(= (mod % 7001) 0))))
(defn now [] (-> (js/Date.) .getTime))
(defn time-slice-in-ms [ms]
(let [start (now)]
(fn [] (-> (now) (- start) (quot ms)))))
; run a single chunk, report results, and schedule
; the next chunk of work in the event loop.
(defn by-chunk-step [chunks calc-fn result-chan]
(doseq [r (calc-fn (first chunks))]
(put! result-chan r))
(js/setTimeout #(by-chunk-step (rest chunks) calc-fn result-chan)))
; transform a set of inputs into chunks, then run
; kick off job to run a calculation function on each chunk.
; meanwhile, immediately return a result-reporting channel.
(defn by-chunk [inputs [ms] calc-fn]
(let [result-chan (chan)
chunks (partition-by (time-slice-in-ms ms) inputs)]
(by-chunk-step chunks calc-fn result-chan)
result-chan))
(defn all-ints [] #(iterate inc 1))
(defn load []
(let [results (by-chunk (all-ints) [20 :ms] find-special-numbers)]
(go-loop []
(println (<! results))
(recur))))
(fw/watch-and-reload :jsload-callback (load))
注意定义all-ints
时有一个问题:重要的是不要保留此列表的头部,否则堆栈使用量会不受限制地增长,浏览器将崩溃。因此all-ints
直接返回函数而不是惰性列表。