请在 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 所做的虚拟恢复(RetryRequest 运行时异常),甚至是 Clojure 本身使用 RetryEx 异常冲破 STM 事务的情况。)

0

评论者:hank

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

0

评论者:hank

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

在我看来,关于闭包和宏的观察需要解释。

0

评论者:hank

进一步的分析:‘delay’表现出相同的行为,并且是一个更简单的案例来检查。对于宏的怀疑是一个误导:如下所示,实际上是由于闭包中的变量神秘地变为 nil,当-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)。在实现中,这可以在这里看到,例如[对于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]。在函数调用的过程中,有一些字段被设置为null,原因是为了让垃圾编译器收集对象,然而这应该在函数成功完成后才做。这可以更改吗?
0

评论者:hank

如第一则评论中描述的'map'函数的情况,解决方案如下:原始的map函数,如果你去掉处理多个coll的case,对分块序列的性能提升以及对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)))))

`

因为所有那些较慢评估的下游函数都是缓存结果的deref类型(更多的lazy-seq,delays,futures,promises),InterruptedException只能在第一次评估期间发生,而尾调用优化(将封闭变量设置为nil,值得一读:[https://clojure.org/lazy](https://clojure.org/lazy))只在第二次调用时发生。第一次仍创建一个捕获序列's'首部的fn,然而这没有被保留,因为它没有被返回。

当我想要中断序列的评估时,我会使用这种特殊的map版本(以及基于lazy-seq的此类等效重写的其他函数,如iterate)。

0

评论者:hank

代替上述修补程序,使用懒惰Y组合子通过实现map和所有其他组合器,并在lazy-seq定义中删除:once元数据标签可以正确地解决这个问题。由于触发:once元数据标签而触发的编译器sort-of-hack清除封闭变量的基本作用是为了递归的情况,这些可以使用Y组合子实现,因此这也将不再需要。

0

评论由:alexmiller 撰写

关于上述延迟的参考,这已在 CLJ-1175 中得到解决,目前正在等待最终评审。

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