【问题标题】:How do you do letcc in Clojure?你如何在 Clojure 中执行 letcc?
【发布时间】:2016-09-20 07:21:00
【问题描述】:

在书The Seasoned Schemer——作者写了如下代码:

(define intersectall
  (lambda (lset)
    (letcc hop
      (letrec
          ((A (lambda (lset)
                (cond
                  ((null? (car lset))  (hop (quote ())))
                  ((null? (cdr lset))  (car lset))
                  (else
                    (intersect (car lset)
                               (A (cdr lset))))))))
        (cond
          ((null? lset)  (quote ()))
          (else  (A lset)))))))

这可能是它在 Clojure 中的样子:

(defmacro letcc
  [name & body]
  `(letfn [(~name [arg#]
             (throw (ex-info (str '~name) {:name '~name :value arg#})))]
     (try ~@body
          (catch clojure.lang.ExceptionInfo e#
            (if (= '~name (:name (ex-data e#)))
              (:value (ex-data e#))
              (throw e#))))))

(defn intersectall
  [lset]
  (letcc hop
   (letfn [(A [lset]
             (cond (empty? (first lset))
                   (hop ())
                   (empty? (rest lset))
                   (first lset)
                   :else
                   (intersect (first lset) (A (rest lset)))))]
     (cond (empty? lset)
           ()
           :else
           (A lset)))))

我的问题是:你在 Clojure 中如何处理 letcc

【问题讨论】:

  • 这个问题有点不清楚。您是在寻找比您更好的letcc,还是想知道它有什么问题?我认为不可能在 Clojure 中做“真正的”letcc,因为它没有一流的延续。

标签: clojure scheme continuations


【解决方案1】:

背景

核心 Clojure 语言不支持一流的延续。这一点,以及 JVM 不提供捕获当前延续的方法这一事实,意味着无法实现满足所有情况的 letcc

但是,在某些情况下可以实现延续。具体来说,如果您拥有所有代码(即必须在其中捕获延续的代码),那么您可以采用延续传递样式 (CPS)。基本上,您为每个函数添加一个额外的参数。此参数是一个函数,表示该调用的继续。您通过调用延续函数“返回”一个值。当然,这种风格自己写起来很痛苦——但幸运的是,这是一种我们可以通过宏轻松应用于特定代码的转换。

CPS 本身不适合不进行尾调用优化 (TCO) 的平台。因为 CPS 中任何函数的最后一步是调用另一个函数,没有 TCO,堆栈会很快溢出,除了最微不足道的计算。这个问题可以通过使用 thunking 和 trampolining 来解决。

解决方案

正如我上面提到的,您可以使用宏编写自己的 CPS 转换。但是,我会邀请您查看我的 pulley.cps 库,它已经为您完成了这项工作。还有其他选择,但据我所知,pulley.cps 是唯一提供以下所有功能的 Clojure 库:

  • call-cc/let-cc
  • “本机”(非转换)代码和转换代码之间的无缝调用
  • 异常 (try/catch/finally) 支持
  • binding 形式(它们也是正确的尾递归!)
  • 允许您提供现有本机函数的 CPS 版本(如果您想在该函数中捕获延续,这是必需的)

替代方案包括:

  • delimc 提供了一个用于分隔延续的库。这似乎不是很完整(例如,binding 失败,因为它不理解 try/finally 块)并且在 4 年内没有被触及。
  • algo.monads 是 Clojure 的 monad 库。 monad 和 continuation 之间有一种强大而有趣的关系,而 algo.monads 提供了一个 continuation monad。尽管 monadic 样式不太方便,但它确实具有使效果更明确的优点,这有助于将使用控制效果的代码与不使用控制效果的代码封装起来。此外,do 表示法(例如,domonad 宏)极大地模糊了直接风格和单子风格之间的界限。

【讨论】:

  • 运行时支持尾调用或 call/cc 以使语言具有这些功能并不是真正的破坏者,因为它可以在编译时伪造它。没有运行时对尾调用和call/cc 提供硬件支持,因此在某种程度上它总是被伪造的。
  • @Sylwester 不确定你的意思。函数调用不能说同样的话吗?我的意思是,硬件可能有一个call 指令(例如,x86 架构),但编译器/运行时仍然必须处理参数传递等。另外,关于 TCO ...尾调用是非尾调用calljmp。所以我想说这两种形式的函数调用都得到了硬件的同等支持。但无论如何,答案的全部意义在于您不必为了实现它们而必须在语言中内置延续。
  • 是或两者都被生成的机器代码同等地模拟,因为过程没有参数。我觉得奇怪的是没有办法实现让所有情况都令人满意的letcc。我在想我们有命名空间并且可以隐藏每一个特殊形式,所以应该有一种方法可以只导入一个延续库并且只需 call/cc 工作而不需要引入新的特殊形式,但当然可以隐藏现有的.
  • @Sylwester,这实际上或多或少是 pulley.cps 中 override-fnoverride-macro! 的目的,尽管它在不需要阴影的情况下实现了效果(在override-fn,它在函数的类上实现了ICallable 协议)。这在某些情况下会有所帮助,但并非在所有情况下都如此。例如,如果您使用 Java 互操作调用 Java 类的方法,则无法捕获该方法中的延续。当然,您可以在 Clojure 和 CPS 转换中实现相同的功能——但是您要重新实现所有 Java 吗?
  • @Sylwester 还记得 Clojure 为各种事物定义了许多 Java 接口,例如数据结构(ISeqIPersistentMapIPersistentVector)。因此,提供这些特征的 CPS 转换并不是很简单。我想可以为这些实现影子接口(我不时考虑过),但基本上你最终会重新实现 Clojure。在某些时候,也许你最好重新启动并构建一种包含延续的语言?无论如何,感谢您的反馈!
【解决方案2】:

(letcc hop ...) 在您的示例中捕获的延续被用作“向上延续”。可以改用名称return(letcc return ... (return () ...)。当调用名为return 的延续时,整个letcc 表达式的计算结果为return 的值——然后作为intersectall 的结果返回。

这意味着 1. 延续上升(我们返回)和 2. 延续只使用一次。满足这些条件后,就可以按照你的做法实现letcctrycatch

所以在我看来,通过编写您的 letcc 宏,您已经回答了自己的问题。

现在正如 Nathan Davis 提到的,还有其他的延续用例,但 Clojure 并不直接支持它们。

注意:这里有一个相关问题:The Seasoned Schemer, letcc and guile

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-05
    相关资源
    最近更新 更多