Common Lisp 中的可参数化 return-from

Parameterizable return-from in Common Lisp

我正在学习 Common lisp 中的块,并做了这个例子来了解块和 return-from 命令是如何工作的:

 (block b1 
            (print 1)
            (print 2)
            (print 3)
            (block b2 
                   (print 4)
                   (print 5)
                   (return-from b1)
                   (print 6)

               )
            (print 7))

它将按预期打印 1、2、3、4 和 5。将 return-from 更改为 (return-from b2) 它将打印 1、2、3、4、5 和 7,正如人们所期望的那样。

然后我试着把它变成一个函数并参数化 return-from:

上的标签
 (defun test-block (arg)  (block b1 
            (print 1)
            (print 2)
            (print 3)
            (block b2 
                   (print 4)
                   (print 5)
                   (return-from (eval arg))
                   (print 6)

               )
            (print 7)))

并使用 (test-block 'b1) 来查看它是否有效,但它没有。有没有没有条件的方法来做到这一点?

我认为这些事情归结为 Common Lisp 中不同类型的名称空间绑定和环境。

第一点是,一个稍有经验的 Lisp 新手可能会尝试修改您尝试的函数,改为说 (eval (list 'return-from ,arg))。这似乎更有意义,但仍然行不通。

命名空间

初学者在类似 scheme 的语言中常犯的一个错误是有一个名为 list 的变量,因为这掩盖了 this 作为函数的顶层定义,并阻止程序员在范围内创建列表这个绑定。 Common Lisp 中的相应错误是当符号仅作为变量绑定时试图将其用作函数。

在 Common Lisp 中有 命名空间,它们是从名称到事物的映射。一些命名空间是:

  • 函数。要获得相应的东西,要么调用它:(foo a b c ...),要么获得静态符号 (function foo)(又名 #'foo)或动态符号 (fdefinition 'foo) 的函数。函数名称可以是符号,也可以是 setf 和一个符号的列表(例如 (serf bar))。符号也可以绑定到此命名空间中的宏,在这种情况下 functionfdefinition 信号错误。
  • 变量。这会将符号映射到相应变量中的值。这也将符号映射到常量。通过写下 foo 或动态地 (symbol-value) 来获取变量的值。符号也可以绑定为 symbol-macro,在这种情况下应用特殊的宏扩展规则。
  • 去标签。这将符号映射到可以 go 的标签(如其他语言中的 goto)。
  • 块。这会将符号映射到您可以 return 的位置。
  • 捕捉标签。这会将对象映射到 catch 它们所在的位置。当您 throw 到一个对象时,实现会有效地查找此命名空间中相应的 catch 并将堆栈展开到它。
  • classes(和结构、条件)。每个 class 都有一个名称,它是一个符号(因此不同的包可能有一个 point class)
  • 包。每个包都由一个字符串和可能的一些昵称命名。此字符串通常是交易品种的名称,因此通常为大写
  • 类型。每种类型都有一个名称,它是一个符号。自然地,class 定义也定义了类型。
  • 声明。引入 declaredeclaimproclaim
  • 可能还有更多。能想到的就这些了

catch-tag 和声明命名空间与其他命名空间不同,因为它们并不真正将符号映射到事物,但它们确实具有如下所述的绑定和环境(请注意,我使用声明来指的是已经声明的东西,比如优化策略或哪些变量是特殊的,而不是命名空间,其中 optimizespecial,实际上 declaration live 看起来太小了包括)。

现在我们来谈谈这种映射可能发生的不同方式。

名称与名称空间中的事物的 绑定 是它们关联的方式,特别是它如何产生以及如何检查.

绑定环境是绑定所在的地方。它说明了绑定的有效期以及可以从哪里访问它。搜索环境以查找与某个名称空间中的某个名称关联的事物。

静态和动态绑定

我们说绑定是 static 如果绑定的名称在源代码中是固定的,绑定是 dynamic 如果名称可以在 运行 时间确定。例如 letblocktagbody 中的标签都引入了静态绑定,而 catchprogv 引入了动态绑定。

请注意,我对 动态 绑定的定义与规范中的定义不同。规范定义对应于我下面的动态环境

顶级环境

这是最后搜索名称的环境,也是顶层定义所在的环境,例如 defvardefundefclass 在此级别运行。这是搜索所有其他适用环境后最后查找名称的地方,例如如果在更近的级别上找不到函数或变量绑定,则搜索该级别。有时可以在定义绑定之前在此级别引用绑定,尽管它们可能会发出警告信号。也就是说,您可以在定义 foo 之前定义一个调用 foo 的函数 bar。在其他情况下,引用是不允许的,例如,在定义包 FOO 之前,您不能尝试实习或读取符号 foo::bar。许多名称空间只允许在顶级环境中进行绑定。这些是

  • 常量(在变量命名空间内
  • classes
  • 类型

虽然(proclaim 除外)所有绑定都是静态的,但可以通过调用 eval 在顶层评估表单来有效地使其动态化。

函数(和[编译器]宏)和特殊变量(和符号宏)也可以定义在顶层。可以使用宏 declaim 静态地或使用函数 proclaim.

动态地在顶层定义声明

动态环境

动态环境 在程序执行期间的一段时间内存在。特别是,动态环境在控制流进入某种(特定类型的)形式时开始,并在控制流离开时结束,通过正常 returning 或通过一些非本地控制转移,如 return-fromgo。要在名称空间中查找动态绑定的名称,将搜索当前活动的动态环境(有效地,即真实系统不会以这种方式实现)从最近到最旧的名称,第一个绑定获胜。

动态环境中绑定了特殊变量和 catch 标签。 Catch 标签使用 catch 动态绑定,而特殊变量使用 let 静态绑定并使用 progv 动态绑定。正如我们稍后将讨论的,let 可以进行两种不同类型的绑定,并且它知道将符号视为特殊符号,如果它是用 defvar 或 'defparameteror if it has been declared asspecial` 定义的。

词汇环境

一个词法环境对应于源代码的一个区域,因为它被编写和一个特定的运行时间实例化。它(稍微松散地)从左括号开始,到相应的右括号结束,并在控制流遇到左括号时实例化。这个描述有点复杂,所以让我们举一个在词法环境中绑定变量的例子(除非它们是特殊的。按照惯例,特殊变量的名称用 * 符号包裹)

(defun foo ()
  (let ((x 10))
    (bar (lambda () x))))
(defun bar (f)
  (let ((x 20))
    (funcall f)))

现在我们调用 (foo) 会发生什么?好吧,如果 x 绑定在动态环境中(在 foobar 中),那么匿名函数将在 bar 中调用,并且第一个动态环境具有绑定 x 将它绑定到 20。

但是这个调用 returns 10 因为 x 绑定在词法环境中所以即使匿名函数被传递给 bar,它会记住对应于foo 的应用程序创建了它,在那个词法环境中,x 被绑定到 10。现在让我们用另一个例子来说明我上面所说的“特定 运行 时间实例化”的意思。

(defun baz (islast)
  (let ((x (if islast 10 20)))
    (let ((lx (lambda () x)))
      (if islast
        lx
        (frob lx (baz t))))))
(defun frob (a b)
  (list (funcall a) (funcall b)))

现在 运行ning (baz nil) 会给我们 (20 10) 因为传递给 frob 的第一个函数会记住外部调用 baz 的词法环境(其中 islastnil)而第二个记住内部调用的环境。

对于不特殊的变量,let 创建静态词法绑定。块名称(由 block 静态引入)、go 标签(tagbody 内的范围)、函数(由 feltlabels)、宏(macrolet) , 和符号宏 (symbol-macrolet) 都在词法环境中静态绑定。来自 lambda 列表的绑定也是词法绑定的。可以在允许的位置之一使用 (declare ...) 或在任何地方使用 (locally (declare ...) ...) 按词法创建声明。

我们注意到所有词法绑定都是静态的。上面描述的 eval 技巧不起作用,因为 eval 发生在顶层环境中,但对词法名称的引用发生在词法环境中。这允许编译器优化对它们的引用以准确知道它们的位置,而无需 运行ning 代码必须携带绑定列表或访问全局状态(例如,词法变量可以存在于寄存器和堆栈中)。它还允许编译器计算出哪些绑定可以在闭包中转义或捕获,并相应地进行优化。一个例外是(符号-)宏绑定在某种意义上可以动态检查,因为所有宏都可以采用 &environment 参数,该参数应传递给 macroexpand(以及其他扩展相关函数)以允许宏扩展器在 compile-time 词法环境中搜索宏定义。

另一件需要注意的事情是,如果没有 lambda-expressions,词法和动态环境的行为方式是一样的。但请注意,如果只有顶级环境,那么递归将无法工作,因为当控制流离开其范围时,绑定将不会恢复。

关闭

当匿名函数从其创建的作用域中逃逸时,该匿名函数捕获的词法绑定会发生什么情况?好吧,有两件事可能发生

  1. 尝试访问绑定导致错误
  2. 匿名函数保留th只要引用它的函数还活着,并且它们可以随意读写它,词法环境就一直活着。

第二种情况称为闭包,发生在函数和变量上。第一种情况发生在与控制流相关的绑定中,因为您不能从已经 returned 的表单中 return。宏绑定都不会发生,因为它们无法在 运行 时间访问。

非本地控制流

在像 Java 这样的语言中,控制(即程序执行)从一个语句流向下一个语句,为 ifswitch 语句分支,为其他语句循环像 breakreturn 这样的特殊语句用于某些类型的跳跃。对于函数,控制流进入函数,直到它最终在函数 returns 时再次出现。转移控制的一种非本地方法是使用 throwtry/catch,如果您执行 throw,那么堆栈将逐个展开,直到找到合适的 catch

在 C 中没有 throwtry/catch 但有 goto。 C 程序的结构秘密地是扁平的,嵌套只是指定“块”以与其开始顺序相反的顺序结束。我的意思是,在 switch 中间有一个 while 循环是完全合法的,循环中有 case 并且 goto 在循环中间是合法的从那个循环之外。有一种方法可以在 C 中进行非本地控制转移:您使用 setjmp 将当前控制状态保存在某处(使用 return 值指示您是否已成功保存状态或只是非本地 return在那里)和 longjmp 到 return 控制流到以前保存的状态。当堆栈展开时,不会发生真正的内存清理或释放,也不需要检查调用堆栈上是否还有调用 setjmp 的函数,因此整个事情可能非常危险。

在 Common Lisp 中,有多种方法可以进行非本地控制转移,但规则更为严格。 Lisp 并没有真正的语句,而是一切都是由表达式树构建的,所以第一条规则是你不能非本地地将控制转移到更深的表达式中,你只能转移出去。让我们看看这些不同的控制转移方法是如何工作的。

blockreturn-from

您已经了解了这些在单个函数中是如何工作的,但还记得我说过块名称是词法范围的。那么这如何与匿名函数交互?

好吧,假设您想在一些大的嵌套数据结构中搜索某些内容。如果您使用 Java 或 C 编写此函数,那么您可能会实现一个特殊的搜索函数来递归遍历您的数据结构,直到找到正确的内容,然后 return 一直向上查找。如果您在 Haskell 中实现它,那么您可能希望将其作为某种折叠来实现,并依靠惰性求值来避免做太多工作。在 Common Lisp 中,您可能有一个函数,它将作为参数传递的其他函数应用于数据结构中的每个项目。现在您可以使用搜索功能调用它。你怎么能得到结果?好吧只是 return-from 到外块。

tagbodygo

A tagbody 类似于 progn 但不是计算正文中的单个符号,它们被称为 tags 并且 tagbody 中的任何表达式都可以 go 给他们以将控制权转移给它。这部分类似于 goto,如果您仍在同一个函数中,但如果您的 go 表达式发生在某个匿名函数中,那么它就像一个安全的词法范围 longjmp.

catchthrow

这些与 Java 模型最相似。 blockcatch 之间的主要区别在于 block 使用词法范围,而 catch 使用动态范围。因此它们之间的关系就像特殊变量和常规变量之间的关系。

终于

在 Java 中,如果由于抛出异常而必须通过它展开堆栈,则可以执行代码来整理东西。这是通过 try/finally 完成的。 Common Lisp 等效项称为 unwind-protect,它确保执行表单,但控制流可能会离开它。

错误

也许值得研究一下错误在 Common Lisp 中是如何工作的。他们使用了哪些方法?

事实证明,答案是错误而不是通常通过调用函数展开堆栈。首先,他们查找所有可能的重启(处理错误的方法)并将它们保存在某个地方。接下来,他们查找所有适用的处理程序(例如,处理程序列表可以存储在特殊变量中,因为处理程序具有动态范围)并一次尝试每一个。处理程序只是一个函数,因此它可能 return(即不想处理错误)也可能不 return。处理程序可能不会 return 如果它调用重新开始。但是重启只是正常功能,那么为什么这些不能 return?好的重启是在出现错误的环境下的动态环境中创建的,因此它们可以直接将控制权从处理程序和抛出错误的代码转移到某些代码以尝试执行某些操作然后继续。重新启动可以使用 goreturn-from 转移控制权。值得注意的是,这里我们有词法范围很重要。递归函数可以在每次连续调用时定义重新启动,因此有必要为变量和 tags/block 名称提供词法范围,以便我们可以确保将控制转移到具有正确状态的调用堆栈上的正确级别.

使用类似 CASE 的条件来 select 从

到 return 的块

推荐的方法是使用 case 或类似的方法。 Common Lisp 不支持从块中计算 returns。它也不支持计算 gos.

使用 case 条件表达式:

(defun test-block (arg)
  (block b1 
    (print 1)
    (print 2)
    (print 3)
    (block b2 
      (print 4)
      (print 5)
      (case arg
        (b1 (return-from b1))
        (b2 (return-from b2)))
      (print 6))
    (print 7)))

无法根据名称计算词法 go 标签、return 块或局部函数

CLTL2 谈到了 go 构造的限制:

Compatibility note: The ``computed go'' feature of MacLisp is not supported. The syntax of a computed go is idiosyncratic, and the feature is not supported by Lisp Machine Lisp, NIL (New Implementation of Lisp), or Interlisp. The computed go has been infrequently used in MacLisp anyway and is easily simulated with no loss of efficiency by using a case statement each of whose clauses performs a (non-computed) go.

由于 goreturn-from 等功能是词法范围的构造,因此不支持计算目标。 Common Lisp 无法在运行时访问词法环境并查询它们。例如,本地功能也不支持此功能。不能在某些词法环境中获取名称并请求具有该名称的函数对象。

动态选择:CATCH 和 THROW

通常效率较低且范围动态的替代方案是 catchthrow。在那里计算标签。