Dan*_*ira 3 lisp eval block 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))
Run Code Online (Sandbox Code Playgroud)
它将按预期打印1、2、3、4和5。将return-from更改为(return-from b2),将打印出1、2、3、4、5和7。
然后,我尝试将其转换为函数,并解析返回位置上的标签:
(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)))
Run Code Online (Sandbox Code Playgroud)
并使用(test-block'b1)来查看其是否有效,但无效。有没有条件可以做到这一点的方法吗?
使用CASE这样的条件来选择要从中返回的块
推荐的方法是使用case或类似方法。Common Lisp不支持从块中计算出的收益。它还不支持计算go的。
使用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)))
Run Code Online (Sandbox Code Playgroud)
一个人不能从名称中计算词汇go标签,返回块或局部函数
CLTL2谈到了该go构造的限制:
兼容性说明:不支持MacLisp的``compute go''功能。Lisp Machine Lisp,NIL(Lisp的新实现)或Interlisp不支持该计算的go的语法特质。无论如何,计算出的go很少在MacLisp中使用,并且通过使用case语句轻松地模拟计算出的go,而不会损失效率,每个case语句的子句都执行(非计算)go。
由于像go和return-from这样的功能都是按词法定义的构造,因此不支持计算目标。Common Lisp无法在运行时访问词汇环境并对其进行查询。例如,本地功能也不支持此功能。在某些词法环境中,不能使用名称来要求具有该名称的函数对象。
动态替代:CATCH和THROW
通常效率较低且动态范围较小的替代方法是catch和throw。在那里计算标签。
我认为这些事情归结为Common Lisp中不同类型的名称空间绑定和环境。
第一点是,经验丰富的新手学习Lisp可能会尝试修改您尝试的功能,(eval (list 'return-from ,arg))而不是说。这似乎更有意义,但仍然行不通。
在诸如方案之类的语言中,一个常见的初学者错误是有一个名为的变量list,这使它成为函数的顶级定义,并且使程序员无法在此绑定的范围内创建列表。Common Lisp中的相应错误是,当符号仅作为变量绑定时,它试图将符号用作函数。
在Common Lisp中,存在命名空间,这些命名空间是从名称到事物的映射。一些名称空间是:
(foo a b c ...),或者获取静态符号(function foo)(aka #'foo)或动态符号的函数(fdefinition 'foo)。函数名称可以是符号,也可以是列表setf和一个符号(例如(serf bar))。可以选择将符号绑定到此命名空间中的宏,在这种情况下function,会出现fdefinition信号错误。foo或者动态记录为(symbol-value)。符号也可以绑定为符号宏,在这种情况下,将应用特殊的宏扩展规则。go(类似于goto其他语言)。catch它们所在的位置。当您throw访问一个对象时,实现会有效地catch在此命名空间中查找对应的对象,并将堆栈展开到该对象。point类)declare,declaim,proclaimcatch-tag和Declarations命名空间与其他命名空间不同,因为它们实际上并未将符号映射到事物,但是它们确实具有以下所述的绑定和环境(请注意,我已经使用声明来引用已经存在的事物)。声明,如优化策略或者是特殊变量,而不是命名空间中,例如optimize,special和确实declaration住这似乎太小,包含)。
现在,让我们讨论一下这种映射可能发生的不同方式。
名称与名称空间中事物的绑定是它们之间关联的方式,特别是名称的外观和检查方式。
该环境一的结合是哪里的地方结合生活。它说明绑定的生存时间以及可以从何处访问绑定。搜索环境以查找与某个名称空间中的某个名称关联的事物。
我们说的绑定是静态的,如果绑定的名称是固定在源代码和绑定是动态的,如果可以在运行时确定的名称。例如let,block在一个标签tagbody都引入静态绑定,而catch并progv引进动态绑定。
请注意,我对动态绑定的定义与规范中的定义不同。规格定义对应于我下面的动态环境。
这是名称中搜索过去的环境,这就是顶级的定义去,例如defvar,defun,defclass在这个水平上运行。在搜索了所有其他适用环境之后,即在此位置最后查找名称,例如,如果在较近的级别上找不到函数或变量绑定,则在该级别上进行搜索。有时可以在定义绑定之前在此级别进行引用,尽管它们可能表示警告。也就是说,您可以定义一个在定义foo之前bar调用的函数foo。在其他情况下,则不允许引用,例如,在定义foo::bar包之前,您不能尝试实习或读取符号FOO。许多名称空间仅允许在顶级环境中进行绑定。这些是
尽管(除外proclaim)所有绑定都是静态的,但可以通过调用eval在顶层评估表单的方式有效地使它们动态化。
函数(和[编译器]宏)和特殊变量(和符号宏)也可以在顶层定义。声明可以通过宏静态定义declaim或通过函数动态定义proclaim。
一个充满活力的环境中存在的程序执行期间的时间区域。特别是,动态环境从控制流进入某种(特定类型的)形式开始,到控制流离开形式时结束,通过正常返回或通过诸如a return-from或的某种非局部控制转移来结束go。为了在名称空间中查找动态绑定的名称,请从最新到最早的名称搜索当前活动的动态环境(有效地,即不会以这种方式实现真实的系统),并且第一个绑定获胜。
特殊变量和catch标签绑定在动态环境中。捕获标记使用catch进行动态绑定,而特殊变量使用进行静态绑定let和动态绑定progv。正如我们将在后面讨论的那样,let可以进行两种不同的绑定,并且知道如果符号已用defvar或'defparameter or if it has been declared asspecial ' 定义,则将其视为特殊符号。
甲词法环境对应的源代码的区域,因为它是写和它的一个特定的运行时实例化。它(稍微松散地)从开头括号开始,到相应的结尾括号结束,并在控制流到达开头括号时实例化。该描述有点复杂,因此让我们举一个在词法环境中绑定变量的示例(除非它们是特殊的。按照惯例,特殊变量的名称包装在*符号中)
(defun foo ()
(let ((x 10))
(bar (lambda () x))))
(defun bar (f)
(let ((x 20))
(funcall f)))
Run Code Online (Sandbox Code Playgroud)
现在,当我们打电话时会发生什么(foo)?好吧,如果x绑定在动态环境中(in foo和bar),则将调用匿名函数,bar并且第一个具有绑定的动态环境x会将其绑定到20。
但是此调用返回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)))
Run Code Online (Sandbox Code Playgroud)
现在运行(baz nil)将为我们提供帮助,(20 10)因为传递给第一个函数的函数frob记住了外部调用baz(where islast是nil)的词法环境,而第二个函数记住了内部调用的环境。
对于非特殊变量,let创建静态词法绑定。块名称(由静态引入block),go标记()中的作用域tagbody,函数(由felt或labels),宏(macrolet)和符号宏(symbol-macrolet)在词汇环境中都是静态绑定的。lambda列表中的绑定也按词法绑定。可以(declare ...)在允许的位置之一或(locally (declare ...) ...)任何位置使用词法创建声明。
我们注意到所有词法绑定都是静态的。eval上面描述的技巧不起作用,因为它eval发生在顶级环境中,但是对词法名称的引用却发生在词法环境中。这使编译器可以优化对它们的引用,以准确地知道它们的位置,而无需运行代码来携带绑定列表或访问全局状态(例如,词法变量可以存在于寄存器和堆栈中)。它还允许编译器确定哪些绑定可以转义或在闭包中捕获或不捕获,并相应地进行优化。一个例外是(符号)宏绑定可以在某种意义上被动态检查,因为所有宏都可以采用&environment应传递给macroexpand (以及其他与扩展相关的功能),以允许宏扩展器在编译时词法环境中搜索宏定义。
要注意的另一件事是,如果没有lambda表达式,则词汇和动态环境的行为方式相同。但是请注意,如果只有顶级环境,则递归将无法工作,因为绑定将无法恢复,因为控制流会离开它们的作用域。
当匿名函数捕获的词汇绑定脱离其创建范围时,该怎么办?好吧,可能会发生两件事
第二种情况称为闭包,发生在函数和变量上。第一种情况发生在与控制流相关的绑定上,因为您无法从已经返回的表单中返回。宏绑定不会发生任何事情,因为它们无法在运行时访问。
在如Java,控制语言(也就是程序执行)从一个声明流向下一个,对分支if和switch语句,循环他人用之类的特殊报表break和return某些种类的跳跃。对于功能,控制流进入功能,直到功能返回时最终再次出现。到传送控制的一个非局部方式是通过使用throw和try/catch其中如果执行一个throw然后,直到合适的叠层是由一块退绕片catch被发现。
在C中,没有throw或try/catch有goto。C程序的结构与嵌套秘密地保持一致,只是指定“块”以与它们开始的顺序相反的顺序结束。我的意思是,在while循环的中间有一个switchcase,在循环的中间有一个循环是完全合法的,而从循环的外部到循环goto的中间是合法的。有一种方法可以在C中进行非本地控制传递:您setjmp可以将当前控制状态保存在某个地方(返回值指示您是否已成功保存状态或只是在本地非本地返回),并且longjmp使控制流返回到先前保存的状态。随着堆栈的展开,不会发生真正的清除或释放内存的情况,无需检查您是否仍然具有setjmp在调用堆栈上调用的函数,因此整个过程可能非常危险。
在Common Lisp中,有多种方法可以进行非本地控制转移,但是规则更为严格。Lisp并没有真正的语句,而是所有的东西都是基于表达式树构建的,因此第一个规则是您不能将控制非本地地转移到更深层的表达式中,而只能转移出去。让我们看看这些不同的控制转移方法是如何工作的。
block 和 return-from您已经了解了它们如何在单个函数中工作,但回想起我说过的块名是按词法作用域划分的。那么这如何与匿名函数交互?
好吧,假设您想在某个大型嵌套数据结构中搜索某些内容。如果您使用Java或C编写此函数,则可以实现特殊的搜索功能以递归遍历您的数据结构,直到找到正确的东西,然后将其全部返回。如果您是在Haskell中实现它,那么您可能希望将其折叠起来,并依靠惰性评估来完成太多工作。在Common Lisp中,您可能有一个函数,该函数将作为参数传递的其他一些函数应用于数据结构中的每个项目。现在,您可以使用搜索功能进行调用。您如何得出结果?好吧,只是return-from到外面的街区。
tagbody 和 goA tagbody就像a,progn但是不是评估体内的单个符号,而是调用它们,tags并且tagbody罐中的任何表达式go都将传递给它们,以将控制权转移给它。这部分类似于goto,如果您仍在同一个函数中,但是如果您的go表达式出现在某个匿名函数中,则就像一个安全的词法范围longjmp。
catch 和 throw这些与Java模型最相似。block和之间的主要区别catch是block使用词汇作用域和catch动态作用域。因此,它们的关系就像特殊变量和常规变量之间的关系一样。
在Java中,如果堆栈由于抛出异常而必须在堆栈中平移,则可以执行代码来整理事情。这是通过完成的try/finally。调用Common Lisp等效项unwind-protect可确保执行表单,但是控制流可能会将其保留。
值得一提的是,Common Lisp中的错误是如何工作的。他们使用哪种方法?
事实证明,答案是错误而不是通常通过调用函数开始展开堆栈。首先,他们查找所有可能的重新启动(处理错误的方式)并将它们保存在某处。接下来,他们查找所有适用的处理程序(例如,处理程序列表可以存储在特殊变量中,因为处理程序具有动态范围),然后一次尝试每个处理程序。处理程序只是一个函数,因此它可能会返回(即不希望处理错误),也可能不会返回。如果处理程序调用重新启动,则可能不会返回。但是重新启动只是正常功能,所以为什么这些不返回?良好的重启是在一个引发错误的动态环境下创建的,因此它们可以将控制权直接从处理程序中移出,并将引发错误的代码传递给某些代码以尝试执行某些操作,然后继续执行。go或return-from。值得注意的是,在这里重要的是要有词汇范围。递归函数可以定义每个后续调用的重新启动,因此有必要为变量和标签/块名称提供词法作用域,以便我们可以确保将控制转移到状态正确的调用堆栈上的正确级别。
| 归档时间: |
|
| 查看次数: |
195 次 |
| 最近记录: |