请在 2024 年 Clojure 状况调查! 中分享您的想法。

欢迎!有关此如何工作的一些更多信息,请参阅关于页面。

0
Clojure

当体包含宏并抛出异常时,lazy-seq 似乎评估不一致。第一次评估抛出异常,随后的返回空序列。

演示代码

`
(defn gen-lazy []
(let [coll [1 2 3]]

(lazy-seq
  (when-let [s (seq coll)]
    (throw (Exception.))))))

(def lazy (gen-lazy))

(try
(println "lazy:" lazy)
(catch Exception ex

(println ex)))

(try
(println "lazy, again:" lazy)
(catch Exception ex

(println ex)))

`

它应该在两次都抛出异常,但实际上只在第一次抛出。一般来说,一个表达式不应当在之前被评估的情况下评估为不同的值。

当移除闭包...

`
(defn gen-lazy []
(lazy-seq

(when-let [s (seq [1 2 3])]
  (throw (Exception.)))))

`

...或者移除 when-let 宏...

`
(defn gen-lazy []
(let [coll [1 2 3]]

(lazy-seq
  (seq coll)
  (throw (Exception.)))))

`

它就能正常工作,即始终抛出异常,因此似乎存在闭包和宏之间的某种交互作用。这种特定的组合在 'map' 函数中使用。

另请参阅: https://groups.google.com/forum/?fromgroups=#!topic/clojure/Z3EiBUQ7Inc

9 答案

0

评论由:hank 提供

注意。我主要使用这个功能的场景是中断在 'map' 表达式中的评估,其中映射的 fn 评估速度较慢(抛出 InterruptedException),因为我不再对它的结果感兴趣。然后我又重新评估它,因为我对最终的结果感兴趣。然而,由于上述错误,惰性序列终止而不是从上次停止的地方重新开始。

(更新:此用例类似于 Jetty 执行的类似替代连续性(RetryRequest 运行时异常)或 Clojure 本身使用 RetryEx 异常强行进入 STM 事务的情况。)

0

评论由:hank 提供

根据 Christophe 的说法,这与 CLJ-457 相关。他的补丁也修复了这个问题。

0

评论由:hank 提供

很抱歉,Christophe 的补丁在这里不起作用。它通过提前抛出异常来避免 LazySeq 被第二次评估。但是 LazySeq 可能会正确地执行第二次,因为导致异常的情况是瞬时的。如上所述,评估可能第一次被中断,抛出 InterruptedException,但第二次不会。

此外,关于闭包和宏的观察也需要解释。

0

评论由:hank 提供

进一步的分析:“delay”表现出了相同的行为,是一个更简单的测试案例。宏的疑虑是一个混淆错误的线索:正如下面所示,实际上是闭包中捕获的变量神奇地变成了 nil,而 when-let 宏只是将其转换为整个表达式的 nil。

`
(def delayed
(let [a true]

(delay
  (print "a=" a)
  (throw (Exception.)))))

(try
(print "delayed 1:“)
(force delayed)
(catch Exception ex (println ex)))

(try
(print "delayed 2:“)
(force delayed)
(catch Exception ex (println ex)))
`

打印

delayed 1:“a= true#
delayed 2:“a= nil#

0
_评论来自:hank_

这导致了可疑的结果:以下表达式在第一次评估时抛出异常,在后续评估时输出 "w00t!"。


(def delayed
  (let [a true]
    (delay
      (if a
        (throw (Exception.))
        "w00t!"))))


试试这样做


(try
  (print "delayed 1:" )
  (println (force delayed))
  (catch Exception ex (println ex)))

(try
  (print "delayed 2:")
  (println (force delayed))
  (catch Exception ex (println ex)))


结果为
delayed 1:#<Exception java.lang.Exception>
delayed 2:w00t!

此代码表明,问题与所怀疑的 :once 标志有关。


(def test-once
  (let [a true]
    (^{:once true} fn* foo []
        (println "a=" a)
        (throw (Exception.)))))


两次调用该 fn 将显示 'a' 从 'true' 变为 'nil',尝试如下


(try
  (print "test-once 1:")
  (单次测试)
  (catch Exception ex (println ex)))

(try
  (打印 "test-once 2:")
  (单次测试)
  (catch Exception ex (println ex)))


结果为
test-once 1:a= true
异常
test-once 2:a= nil
异常

当移除 ^{:once true} 的时候,上述情况不会发生。现在有人可能会说上述函数被调用两次,这恰恰是带上 :once 标志时不应该做的事情,但我认为不成功的调用不能算作对 :once 标志的调用。延迟和懒惰序列宏也同意我的看法,因为如果在体中抛出异常,结果对象不会被实现(按照 realized? 函数),因此延迟/延迟序列/懒惰序列的重新评估会重复评估体中的实现/评估。

在上面的代码第一次评估后,尝试使用 (realized? delayed)。在这一实施中,这可以看做,例如,[在这里为 clojure.lang.Delay|https://github.com/clojure/clojure/blob/d0c380d9809fd242bec688c7134e900f0bbedcac/src/jvm/clojure/lang/Delay.java#L33](同样适用于惰性序列),在體函數在成功调用后才設置為 null(表示已實現)。

:once 标志只影响[编译器的这部分|https://github.com/clojure/clojure/blob/d0c380d9809fd242bec688c7134e900f0bbedcac/src/jvm/clojure/lang/Compiler.java#L4701]。在函数调用过程中设置了某个字段为 null,这在允许垃圾收集器回收对象方面是一个很好的做法,然而,这应该在函数成功完成后才完成。这可以更改吗?
0

评论由:hank 提供

对于第一份评论中描述的 'map' 函数的情况,一个解决方案如下:原始的 map 函数,如果你去掉用于多个 coll 的情况,对分块序列的性能改进以及将 coll 参数强制转换为序列,看起来是这样的

`
(defn map [f s]
(lazy-seq

(cons (f (first s)) (map f (rest s)))))

`

在我的解决方案中,我评估 f 两次

`
(defn map [f s]
(lazy-seq

(f (first s))
(cons (f (first s)) (map f (rest s)))))

`

由于下游函数的评估都较慢,且缓存了结果(更多懒惰序列、延迟、将来、承诺),InterruptedException只能在第一次评估期间发生,尾调用优化将闭包变量设置为 nil(这里读一下也有帮助:https://clojure.org/lazy),而只发生在第二次调用时。第一次仍然创建了一个捕获序列 's' 的头部的一个 fn,然而这不会被保留,因为它没有被返回。

当我需要中断性、可重启序列评估时,我会使用这个特殊的 map 版本(以及其他基于惰性序列的类似重写的函数,如 iterate)。

0

评论由:hank 提供

与上述黑客攻击相反,使用抽头 Y-结合符 implement map 和所有其他结合符,并在 lazy-seq 定义中删除 :once 元数据标签可以正确地解决这个问题。编译器使用 :once 元数据标签触发的清除闭包变量的 sort-of-hack 基本上只适用于可以用 Y-结合符实现的重递归情况,那么它也不再需要了。

0

评论者:alexmiller

关于上面的延迟参考,这在 CLJ-1175 中有所说明(并得到了解决),它正在等待最后阶段的处理。

0
参考:https://clojure.atlassian.net/browse/CLJ-1119(由 alex+import 报告)
...