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' 表达式的评估,其中映射的函数 evaluations 慢(例如,引发 InterruptedException),因为我对它不再感兴趣。然后以后重新评估它,因为我对最终的结果又感兴趣了。然而,因为上面的错误,懒序列终止而不是从上次离开的地方重新开始。

(更新:这个用例类似于 Jetty 所做的仿生续发(RetryRequest 运行时异常)或 Clojure 自身使用 RetryEx 异常闯入 STM 事务的情况。)

0

评论者:hank

根据克洛德的说法,这与CLJ-457有关。他的补丁也修复了这个问题。

0
by

评论者:hank

抱歉,克洛德的补丁在我这里不起作用。它通过提前抛出异常来避免对LazySeq进行第二次评估。然而,由于导致异常的情况是瞬时的,LazySeq可能在进行第二次时进行正确的评估。根据上面的评论,在第一次评估时可能会中断评估,抛出InterruptedException,而在第二次不抛出。

据我个人看法,关于闭包和宏的观察也需要解释。

0
by

评论者: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
by
_由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}时,这种情况不会发生。现在有人可能会争辩说上面的函数被调用了两次,这正是:once标志不应该做的事情,但我争辩说失败的调用不应计入:once标志的调用次数。延迟和lazy-seq宏也同意我的观点,因为如果评估体抛出异常,则产生的对象不会被实现(如realized?函数所示),因此,在延迟/lazy-seq的重新评估中,将重复评估/实现评估体。

尝试在上述代码的第一次评估之后使用(实现?延迟)。在实现中,例如可以通过[这里查看 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-seq、delays、futures、promises),InterruptedException 只能在第一次评估期间发生,而设置闭合变量为 nil 的尾调用优化(值得在这里阅读:https://clojure.org/lazy)只能在第二次调用时发生。第一次仍然创建一个捕获序列 's' 的 Head 的 fn,但是因为它不返回,所以没有被保留。

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

0
通过

评论者:hank

代替上面的伎俩,实现 map 和所有其他组合器使用懒 Y-combinator,并从 lazy-seq 定义中移除 :once 元数据标记,可以正确地解决问题。由于编译器通过 :once 元数据标签触发的 hack 类似于只为可以由 Y-combinator 实现的递归情况,那也不会再需要了。

0
通过

评论由:alexmiller 提出

关于上述 Delay 引用,这在 CLJ-1175 中已涵盖(并解决),该补丁正在等待最终审核。

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