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' 表达式,在映射的函数计算缓慢时(抛出 InterruptedException),因为我已经不再对它的结果感兴趣。然后我稍后再次评估它,因为我想知道它的结果无论如何,但是因为有上述错误,懒序列结束而不是从上次结束的地方重新启动。

(更新:这个用例类似于 Jetty 做的模拟 Continuation(RetryRequest runtime exception)或 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.)))))


调用函数两次将显示'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宏在这一点上与我意见一致,因为如果体(body)计算抛出异常,则结果对象不会被实现(根据realized?函数),因此延迟/lazy-seq的重评估会导致体的实现/评估被重复。

在上面的代码第一次评估后尝试使用(realized? delayed)。在实现中,这可以通过以下链接查看,比如这里[clojure.lang.Delay](https://github.com/clojure/clojure/blob/d0c380d9809fd242bec688c7134e900f0bbedcac/src/jvm/clojure/lang/Delay.java#L33)(对于LazySeq类似),只有在调用成功返回后才将body-fn设置为null(意味着实现)。

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

评论者: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)))))

`

因为下游的函数都是执行缓慢的deref类型,并且缓存它们的结果(更多的lazy-seqs、delays、futures、promises),InterruptedException只能发生在第一次评估期间,而设置闭包变量为nil的尾调用优化(它值得在这里阅读:https://clojure.org/lazy)只发生在第二次调用。第一次仍然创建了一个捕获序列's'头的fn,然而这不会被保留,因为它没有被返回。

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

0

评论者:hank

而不是上面那种hack,使用类似于懒Y-combinator的懒映射和移除lazy-seq定义中的:once元数据的实现可以正确解决这个问题。由于由于:once元数据标签触发的清除闭包变量的编译器sort-of-hack基本上只为可以使用Y-combinator执行的递归情况而存在,因此这也将不再需要。

0

评论者:alexmiller

关于延迟引用,这已在 CLJ-1175 中得到解决(并予以处理),目前正在等待最后审查。

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