在小调查中分享你的想法:[2024 Clojure现状调查!](https://www.surveymonkey.com/r/clojure2024)

欢迎!请参阅关于页面了解更多此平台的工作信息。

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](https://groups.google.com/forum/?fromgroups=#!topic/clojure/Z3EiBUQ7Inc)

9 答案

0

评论者为:hank

注意:我使用这个的主要场景是中断评估一个 'map' 表达式,其中映射函数的评估很慢(抛出 InterruptedException),因为我已经不再关心它的结果。然后我稍后重新评估它,因为我又对此结果感兴趣,然而由于这个漏洞,懒序列终止而不是从上次停止的地方继续。

(更新:此用例类似于Jetty(RetryRequest运行时异常)或Clojure本身在STM事务中使用RetryEx异常时的那种虚假延续。)

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:")
  (test-once)
  (catch Exception ex (println ex)))

(try
  (print "test-once 2:")
  (test-once)
  (catch Exception ex (println ex)))


结果为
test-once 1:a= true
#<Exception java.lang.Exception>
test-once 2:a= nil
#<Exception java.lang.Exception>

当移除^{:once true}时,这种情况不会发生。现在有人可能会争论上面的fn被调用了两次,这与使用:once标志时所期望的行为正好相反,但我主张在失败的调用中并不算作:once标志的调用。延迟和lazy-seq宏也同意这一点,因为如果体内部的表达式抛出异常,则会认为结果对象没有得到实现(按照 realized? 函数的定义),因此当延迟/lazy-seq在重新评估时会重复实现/评估体内部。

在上面的代码首次评估后使用(realized? delayed)进行尝试。在实现中,这可以在例如[这里](https://github.com/clojure/clojure/blob/d0c380d9809fd242bec688c7134e900f0bbedcac/src/jvm/clojure/lang/Delay.java#L33)(对LazySeq similarly)看到,只有在调用的成功返回后,body-fn才会被设置为null(表示实现)。

:once标志只影响[编译器这部分|https://github.com/clojure/clojure/blob/d0c380d9809fd242bec688c7134e900f0bbedcac/src/jvm/clojure/lang/Compiler.java#L4701]。在函数调用过程中,某个字段被设置为null,这对于让垃圾收集器收集对象来说是一个很好的理由。然而,这只应在函数成功完成后进行。这可以更改吗?
0
by

评论者为:hank

对于第一篇评论中描述的'map'函数的情况,一个补救方案如下:原始的map函数,如果你移除了多个coll的情况,针对分块seq的性能提升,以及强制coll参数成为seq的情况,看起来是这样的

`
(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)))))

`

因为所有后续的函数都是慢速评估的类型,这些函数会缓存结果(更多的lazy-seqs,delays,futures,promises),InterruptedException只能在第一次评估期间发生,而将闭包变量设置为nil的尾调用优化(在此处阅读值得一读:https://clojure.org/lazy)只会在第二次调用时发生。第一次仍然会创建一个捕获序列's'头部的fn,但这个fn不会被保留,因为它没有返回。

当我想中断并重新启动seq评估时,我会使用这个特殊的map版本(以及其他基于lazy-seq类似重写的函数)。

0
by

评论者为:hank

与其上面的技巧,用lazy Y-combinator实现map及其所有其他组合器,并从lazy-seq定义中移除:once元数据标签,可以正确地解决问题。编译器中的奇怪的技巧,它的触发是由:once元数据标签引起的,基本是用来清理闭包变量,这是可以通过Y-combinator实施的递归情况,所以这也将不再需要。

0
by

评论者:alexmiller

关于延迟引用,这已在CLJ-1175中解决(并得到回应),该问题正在等待最终审查。

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