如果我使用map
对特定于懒惰序列的每个成员的各个数据结构执行副作用/变异操作,我是否需要(a)首先调用doall
,以强制实现在执行命令操作之前的原始序列,或者(b)调用doall
以强制在我将结果序列上的功能操作映射之前发生副作用?
我相信当任何序列的元素之间没有依赖关系时,不需要doall
,因为map
无法将函数应用于成员直到产生该序列的map
s的函数已应用于较早序列的相应元素的序列。因此,对于每个元素,函数将以适当的顺序应用,即使其中一个函数产生后续函数所依赖的副作用。 (我知道我不能假设任何元素 a 将在元素 b 之前被修改,但这并不重要。)
这是对的吗?
这是一个问题,如果它足够清楚,那么就没有必要进一步阅读了。其余部分描述了我试图更详细的内容。
我的应用程序有一系列的defrecord结构("代理"),每个结构都包含一些core.matrix向量(vec1
,vec2
)和core.matrix矩阵( mat
)。假设为了速度,我决定(破坏性地,非功能性地)修改矩阵。
该程序通过调用map
三次,为每个代理执行以下三个步骤,将每个步骤应用于每个代理。
vec1
assoc
mat
(即矩阵将保留不同的状态)。vec2
更新每个代理中的向量assoc
。例如,persons
是一个序列,可能是懒惰的(编辑:添加外doall
s):
(doall
(->> persons
(map #(assoc % :vec1 (calc-vec1 %))) ; update vec1 from person
(map update-mat-from-vec1!) ; modify mat based on state of vec1
(map #(assoc % :vec2 (calc-vec2-from-mat %))))) ; update vec2 based on state of mat
可替换地:
(doall
(map #(assoc % :vec2 (calc-vec2-from-mat %)) ; update vec2 based on state of mat
(map update-mat-from-vec1! ; modify mat based on state of vec1
(map #(assoc % :vec1 (calc-vec1 %)) persons)))) ; update vec1 from person
请注意,任何代理的状态都不取决于任何其他代理的状态。我是否需要添加doall
s?
编辑:截至2014年4月16日的答案概述:
我建议阅读所有给出的答案,但看起来好像有冲突。他们没有,我认为如果我总结一下主要想法可能会有用:
(1)我的问题的答案是"是":如果,在我描述的过程结束时,一个导致整个懒惰序列被实现,那么对每个元素所做的将是根据步骤(1,2,3)的正确顺序发生。没有必要在步骤2之前或之后应用doall
,其中每个元素的数据结构都会发生变异。
(2)但是:这是一个非常糟糕的主意;你将来要麻烦了。如果在某些时候你无意中最终意外地实现了序列的全部或部分,而不是你最初想要的那样,那么后面的步骤可能会得到数据结构中的值,这些值是在错误的时间放置的 - 在你不期望的时间。改变每个元素数据结构的步骤不会发生,直到实现了懒惰seq的给定元素,所以如果你在错误的时间意识到它,你可能会在后面的步骤中得到错误的数据。这可能是一种非常难以追踪的错误。 (感谢@ A.Webb非常清楚这个问题。)
答案 0 :(得分:3)
使用极端谨慎混合懒惰与副作用
(defrecord Foo [fizz bang])
(def foos (map ->Foo (repeat 5 0) (map atom (repeat 5 1))))
(def foobars (map #(assoc % :fizz @(:bang %)) foos))
现在我的foobars fizz现在是1?
(:fizz (first foobars)) ;=> 1
很酷,现在我将单独留下foobars并与原来的foos一起工作......
(doseq [foo foos] (swap! (:bang foo) (constantly 42)))
让我们检查foobars
(:fizz (first foobars)) ;=> 1
(:fizz (second foobars)) ;=> 42
...糟糕
一般情况下,使用doseq
而不是map
来表示副作用,或者在实现之前了解延迟副作用的后果。
答案 1 :(得分:1)
如果您稍后在程序中对结果执行某些操作,则无需向doall
添加任何调用。例如,如果你运行上面的地图,并没有对结果做任何事情,那么将不会实现任何元素。另一方面,如果你仔细阅读结果序列,例如打印它,那么你的每一个计算将按顺序在每个元素上按顺序发生。这就是步骤1,2和3将发生在输入序列中的第一件事,然后步骤1,2和3将发生在第二件,依此类推。没有必要预先实现序列以确保值可用,懒惰评估将负责这一点。
答案 2 :(得分:1)
您不需要在两个doall
操作之间添加map
。但除非你在REPL中工作,否则你需要添加doall
或dorun
来强制执行你的懒惰序列。
这是事实,除非你关心操作的顺序。
让我们考虑以下示例:
(defn f1 [x]
(print "1>" x ", ")
x)
(defn f2 [x]
(print "2>" x ", ")
x)
(defn foo [mycoll]
(->> mycoll
(map f1)
(map f2)
dorun))
默认情况下,clojure将获取mycoll
的第一个块并将f1
应用于此块的所有元素。然后,它会将f2
应用于生成的块。
因此,如果mycoll
如果是list
或普通的懒惰序列,您会看到f1
和f2
依次应用于每个元素:
=> (foo (list \a \b))
1> a , 2> a , 1> b , 2> b , nil
或
=> (->> (iterate inc 7) (take 2) foo)
1> 7 , 2> 7 , 1> 8 , 2> 8 , nil
但是,如果mycoll
是vector
或者是懒惰的序列,那么您会看到完全不同的东西:
=> (foo [\a \b])
1> a , 1> b , 2> a , 2> b , nil
尝试
=> (foo (range 50))
并且您将看到它以32个元素处理块中的元素。
所以,小心使用带有副作用的延迟计算!
以下是一些提示:
始终以doall
或dorun
结束命令以强制进行计算。
使用doall
和comp
来控制计算顺序,例如:
(->> [\a \b]
; apply both f1 and f2 before moving to the next element
(map (comp f2 f1))
dorun)
(->> (list \a \b)
(map f1)
; process the whole sequence before applying f2
doall
(map f2)
dorun)
答案 3 :(得分:0)
map
总是产生一个惰性结果,即使对于非惰性输入也是如此。如果您需要强制执行一些必要的副作用,您应该在doall
的输出上调用dorun
(或map
,如果序列永远不会被使用,并且只对副作用进行映射) (例如,在关闭之前使用文件句柄或数据库连接。)
user> (do (map println [0 1 2 3]) nil)
nil
user> (do (doall (map println [0 1 2 3])) nil)
0
1
2
3
nil