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

请注意。我使用这个的主要场景是在映射表达式求值缓慢(抛出InterruptedException)时中断其评估,因为我不再对此结果感兴趣。然后我稍后再次评估它,因为我最终还是对结果感兴趣,然而在这个错误的影响下,惰性序列(lazy-seq)终止而不是从上次离开的地方重新开始。

(更新:这个使用场景类似于Jetty用(RetryRequest运行时异常)做得那种伪造的断点,或者是Clojure用RetryEx异常在STM事务中断中的处理方式。)

0 投票

评论者:hank

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

0 投票

评论者:hank

很抱歉,在本地环境中 Christophe 的补丁不起作用。它通过提前抛出异常来避免第二次评估 LazySeq。但是,由于导致异常的情况是瞬时的,LazySeq 第二次可能正确评估。根据上面的评论,第一次评估可能会被中断,抛出 InterruptedException,但第二次则不会。

我认为,关于闭包和宏的观察也需要解释。

0 投票

评论者:hank

进一步洞察:"延迟"表现出相同的行为,是一个更容易检查的简单案例。关于宏的怀疑是一个红鲱鱼:如下所示,实际上是因为闭包中被魔术后变量变为 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 投票
— comment made 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.)))))


连续调用 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),体部fn在调用返回成功后才设置为null(表示实现)。

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

评论者:hank

第1条评论中描述的“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](https://clojure.org/lazy))只发生在第二次调用上。第一次仍然创建一个捕获序列's'头部的fn,但这并不被保留,因为它没有返回。

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

0 投票

评论者:hank

而不是上述技巧,使用懒惰Y-combinator来实现map以及所有其他组合子,并从lazy-seq的定义中移除:once元数据标参照,可以解决所有问题。由于编译器稍微“黑客”式的清除由:once元数据标参照触发的封闭变量是基于可以由Y-combinator实现的情况,因此这也不再需要了。

0 投票

评论者:alexmiller

关于上面提到的延迟参考,这在 CLJ-1175 中已经(并得到)解决,该问题正在等待最终筛查。

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