需要在元素明确的命令操作之前/之后强制实现延迟seqs吗?

时间:2014-04-14 16:53:42

标签: clojure lazy-evaluation

如果我使用map对特定于懒惰序列的每个成员的各个数据结构执行副作用/变异操作,我是否需要(a)首先调用doall,以强制实现在执行命令操作之前的原始序列,或者(b)调用doall以强制在我将结果序列上的功能操作映射之前发生副作用?

我相信当任何序列的元素之间没有依赖关系时,不需要doall,因为map无法将函数应用于成员直到产生该序列的map s的函数已应用于较早序列的相应元素的序列。因此,对于每个元素,函数将以适当的顺序应用,即使其中一个函数产生后续函数所依赖的副作用。 (我知道我不能假设任何元素 a 将在元素 b 之前被修改,但这并不重要。)

这是对的吗?

这是一个问题,如果它足够清楚,那么就没有必要进一步阅读了。其余部分描述了我试图更详细的内容。


我的应用程序有一系列的defrecord结构("代理"),每个结构都包含一些core.matrix向量(vec1vec2)和core.matrix矩阵( mat)。假设为了速度,我决定(破坏性地,非功能性地)修改矩阵。

该程序通过调用map三次,为每个代理执行以下三个步骤,将每个步骤应用于每个代理。

  1. 使用vec1
  2. 在功能上更新每个代理中的向量assoc
  3. 根据前面的向量修改每个代理中的矩阵mat(即矩阵将保留不同的状态)。
  4. 根据步骤2生成的矩阵状态,使用vec2更新每个代理中的向量assoc
  5. 例如,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非常清楚这个问题。)

4 个答案:

答案 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中工作,否则你需要添加doalldorun来强制执行你的懒惰序列。

这是事实,除非你关心操作的顺序。

让我们考虑以下示例:

(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或普通的懒惰序列,您会看到f1f2依次应用于每个元素:

=> (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

但是,如果mycollvector或者是懒惰的序列,那么您会看到完全不同的东西:

=> (foo [\a \b])
1> a , 1> b , 2> a , 2> b , nil

尝试

=> (foo (range 50))

并且您将看到它以32个元素处理块中的元素。

所以,小心使用带有副作用的延迟计算

以下是一些提示:

始终以doalldorun结束命令以强制进行计算。

使用doallcomp来控制计算顺序,例如:

(->> [\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