请分享您的看法,参加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 runtime exception)的情况,或者是 Clojure 本身使用 RetryEx 异常强行推进 STM 事务的情况。)

0
by

评论作者:hank

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

0
by

评论作者:hank

对我这里来说,Christophe的补丁不起作用。它通过提前抛出异常避免了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}时不会发生这种情况。现在有人可以争论说上面的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](similarly for LazySeq),仅在invocation返回*成功*后才将body-fn设置为null(意味着realized)来实现(对LazySeq亦然)。

:once标志仅影响[编译器这部分|https://github.com/clojure/clojure/blob/d0c380d9809fd242bec688c7134e900f0bbedcac/src/jvm/clojure/lang/Compiler.java#L4701]。在函数调用过程中,某个字段被设置为nil,理由是让垃圾编译器收集对象,然而这应该在函数成功完成后才能做。这能改变吗?
0
by

评论作者:hank

第一则评论中描述的'map'函数的情况的变通方法如下:原始的map函数,如果你移除处理多个coll的情况,为分块seqs的性能增强和对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)))))

`

由于下游函数 gradual-evaluation 都是deref类型的缓存其结果(更多lazy-seq、delays、futures、promises),InterruptedException只可能在第一次评估期间发生,而尾调用优化将封闭的变量设置为零(值得在这里阅读: https://clojure.org/lazy)仅发生在第二次调用时。第一次创建了一个捕获seq“s”的头部fn,但它没有保留,因为它没有返回。

我使用这个特殊的map版本(以及其他基于lazy-seq类似重写的函数,例如iterate)来进行可中断、可重启的seq评估。

0
by

评论作者:hank

而不是上面的诡计,使用懒惰的Y-combinator实现map和其他所有组合子,并从lazy-seq的定义中删除:once元数据标记,

0
作者:

评论者:alexmiller

关于上述延迟引用,这些问题已在 CLJ-1175 中得到解决(并得到了处理),该请求正在等待最终审查。

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