码迷,mamicode.com
首页 > 其他好文 > 详细

简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation

时间:2014-04-30 01:06:44      阅读:562      评论:0      收藏:0      [点我收藏+]

标签:com   http   blog   style   class   div   img   code   size   log   tar   

当我写到这里的时候,我自己都吃了一惊。 环境、存储这些比较让人耳熟的还没讲到,continuation先出来了。 维基百科里对continuation的翻译是“延续性”。 这翻译看着总有些违和感而且那个条目也令人不忍直视。 总之continuation似乎没有好的中文翻译,仿佛中国的计算机科学里没有continuation这个概念似的。

Continuation这个概念相当于过程式语言里的函数调用栈。 它是用于保存“现在没空处理,待会再处理的事”的数据结构。 这样说有点抽象,举个例子,函数应用那条表达式的求值过程(call-by-value)是这样:

  eval((MN))bubuko.com,布布扣                 bubuko.com,布布扣bubuko.com,布布扣=bubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣eval(L[Xeval(N)])bubuko.com,布布扣其中eval(M)=λX.Lbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣
这个过程依次做这三件事:

  1. 计算eval(M)bubuko.com,布布扣 ,结果是λX.Lbubuko.com,布布扣
  2. 计算eval(N)bubuko.com,布布扣
  3. 做替换L[Xeval(N)]bubuko.com,布布扣

解释器一次只能计算一个表达式,所以当它计算eval(M)bubuko.com,布布扣 前,要先把第二步和第三步要做的事备忘到continuation。

假设Mbubuko.com,布布扣 还是个函数调用M=(Mbubuko.com,布布扣1bubuko.com,布布扣Mbubuko.com,布布扣2bubuko.com,布布扣)bubuko.com,布布扣 。 那么,计算eval(M)bubuko.com,布布扣 又要分三步: 先计算eval(Mbubuko.com,布布扣1bubuko.com,布布扣)=λX.Lbubuko.com,布布扣1bubuko.com,布布扣bubuko.com,布布扣 ,第二步计算eval(Mbubuko.com,布布扣2bubuko.com,布布扣)bubuko.com,布布扣 ,第三步做替换Lbubuko.com,布布扣1bubuko.com,布布扣[Xeval(Mbubuko.com,布布扣2bubuko.com,布布扣)]bubuko.com,布布扣 。 解释器只能先计算eval(Mbubuko.com,布布扣1bubuko.com,布布扣)=λX.Lbubuko.com,布布扣1bubuko.com,布布扣bubuko.com,布布扣 ,将第二步计算eval(Mbubuko.com,布布扣2bubuko.com,布布扣)bubuko.com,布布扣 ,第三步做替换Lbubuko.com,布布扣1bubuko.com,布布扣[Xeval(Mbubuko.com,布布扣2bubuko.com,布布扣)]bubuko.com,布布扣 备忘到continuation。 于是,continuation备忘的事情按待处理的顺序依次是:

  1. 计算eval(Mbubuko.com,布布扣2bubuko.com,布布扣)bubuko.com,布布扣
  2. 做替换Lbubuko.com,布布扣1bubuko.com,布布扣[Xeval(Mbubuko.com,布布扣2bubuko.com,布布扣)]bubuko.com,布布扣
  3. 计算eval(N)bubuko.com,布布扣
  4. 做替换L[Xeval(N)]bubuko.com,布布扣

可以看到,continuation是一个FILO(先进后出)的数据结构,也就是栈。

在之前的解释器里并没有提到continuation,但是解释器仍然能正常工作, 这是因为解释这个解释器的解释器——Racket解释器——帮我们管理了continuation。 这一节的目标是自己管理continuation。

从一个简单的递归函数做起

先说一个概念:尾调用。 假设一个函数体中有一句函数调用表达式(fn)bubuko.com,布布扣 ,这个函数调用被称为尾调用,如果这条表达式是函数体里最后求值的表达式。 如λn.(fn)bubuko.com,布布扣 ,和λn.(if(iszeron)0(fn))bubuko.com,布布扣 中的(fn)bubuko.com,布布扣 都是尾调用。 而λn.(+1(fn))bubuko.com,布布扣 中的(fn)bubuko.com,布布扣 不是尾调用,因为计算完(fn)bubuko.com,布布扣 后还要再算个加法。 一个简单的判断是否尾调用的方法是看这条调用表达式是不是不在一个参数位置(let表达式和if表达式先做宏展开)。

学过C或者C++都知道,调用函数的时候是要保存信息到函数调用栈的(我上学时学的这俩,不知道其他语言的课上会不会讲函数调用栈)。 尾调用的特点是:尾调用理论上不需要保存信息到函数调用栈——实际上也不需要,但是有些语言就不支持,比如说Python。 换句话说,尾调用不会导致continuation增长。 这个很好理解,因为尾调用是最后求值的表达式了,不需要备忘后面要做的事(后面没有事了),所以也就不会导致continuation增长。 后面会从continuation的角度说明为什么尾调用不会令continuation增长。

题外话: 刚才提到“理论上”和“实际上”。 经常能听到这种句式:理论上怎么怎么,可惜,实际上是吧啦吧啦。 其中“吧啦吧啦”是一句对“怎么怎么”的否定。 我觉得这是反科学以及读书无用论的潜意识在作祟,另外有时候是用来逃避责任的借口。 理论上怎么样,实际上就应该怎么样。 如果有套理论的结论和现实的结果不同,那么这就不能称为理论,最多算个猜想,而且是个错误的猜想。

如果一个尾调用是一个递归调用,那么就称为尾递归。 尾递归又叫迭代。 我们现在要做的事其实就是把一个递归程序(之前写的解释器)中的非尾递归全改成尾递归, 也就是递归转迭代

为了熟悉递归转迭代的套路,先拿一个简单的递归函数练练手。 这个函数就是之前用到的double函数:

  double(0)bubuko.com,布布扣  double(n)bubuko.com,布布扣bubuko.com,布布扣=bubuko.com,布布扣=bubuko.com,布布扣bubuko.com,布布扣0bubuko.com,布布扣2+double(n?1),其中n0bubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣
计算double函数的过程有一个状态量:当前计算的表达式double(n)bubuko.com,布布扣 或者一个数字nbubuko.com,布布扣 (计算结果)。 现在加入另一个状态量continuation,记为κbubuko.com,布布扣 (因为continuation第一个字母c发音k,所以用了个长的像k的希腊字母……)。 计算double函数的过程只有第二行是递归过程,备忘的事是加2。 κbubuko.com,布布扣 定义为:
  κbubuko.com,布布扣         bubuko.com,布布扣bubuko.com,布布扣=bubuko.com,布布扣|bubuko.com,布布扣bubuko.com,布布扣mtbubuko.com,布布扣?κ,+2?bubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣
mt表示空的continuation,也就是说没有后续的事情了。

加入continuation后的求值过程如下:

  ?double(0),κ?bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣  ?double(n),κ?bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣                                        bubuko.com,布布扣  ?n,?κ,+2??bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣                                     bubuko.com,布布扣  ?n,mt?bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣?0,κ?bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣?double(m),?κ,+2??bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣其中n0,m=n?1bubuko.com,布布扣?m,κ?bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣其中m=n+2bubuko.com,布布扣输出nbubuko.com,布布扣bubuko.com,布布扣bubuko.com,布布扣
下面解释一下。 计算开始时的状态表示为?double(n),mt?bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣 。 下标v表示第一个状态量是一个表达式,要对这个表达式求值。 用箭头bubuko.com,布布扣 表示一步,带下标v的箭头bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣 表示这一步对表达式求值, 带下标c的箭头bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣 表示这一步从continuation中取出一件备忘的事执行。 对于n0bubuko.com,布布扣 ?double(n),κ?bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣 的下一步要做的是,计算double(n?1)bubuko.com,布布扣 ,同时将加2备忘到κbubuko.com,布布扣 , 也就是?double(m),?κ,+2??bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣 ,其中m=n?1bubuko.com,布布扣 。 当n=0bubuko.com,布布扣 时,double(0)bubuko.com,布布扣 求得值0bubuko.com,布布扣 ,所以?double(0),κ?bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣vbubuko.com,布布扣?0,κ?bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣 。 用下标c表示第一个状态是一个数字,下一步该从continuation取出备忘的事来办了。 最后是?n,κ?bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣 的情况,如果κbubuko.com,布布扣 不是空:κ=?κbubuko.com,布布扣bubuko.com,布布扣,+2?bubuko.com,布布扣 ,就加2到nbubuko.com,布布扣 上; 如果κbubuko.com,布布扣 是空:κ=mtbubuko.com,布布扣 ,说明continuation没事了,而这时表达式也求完了,于是返回最终结果nbubuko.com,布布扣

然后是写代码。 针对bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣 bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣 需要两个函数。 函数value-of/k是bubuko.com,布布扣vbubuko.com,布布扣bubuko.com,布布扣 (Lisp命名规范里一般用斜杠表示with的意思)。 函数apply-cont是bubuko.com,布布扣cbubuko.com,布布扣bubuko.com,布布扣

bubuko.com,布布扣

bubuko.com,布布扣

上面代码中用Lisp里的list数据结构来保存continuation。 我们可以不用list来保存continuation。 Continuation备忘的是“待做的事”。 这个“待做的事”可以理解为一个过程,也就是一个函数。 所以,可以用函数来保存continuation!

空mt是一个直接返回参数的函数(lambda (v) v)。 ?κ,+2?bubuko.com,布布扣 先加2,然后应用κbubuko.com,布布扣 :(lambda (v) (apply-cont cont v)),其中cont是κbubuko.com,布布扣 。 完整代码:

bubuko.com,布布扣

用函数来保存continuation的写法看起来蛮像回调函数(callback)。 Lisp的一个噱头是程序与数据统一对待,这也算是一个体现吧。 用函数还能实现对象(面向对象编程的对象),这是题外话了。 用函数保存数据一个好处是熟悉这种方法后写起来很方便; 坏处是扩展访问方式比较麻烦,并且调试的时候不能打印详细信息。

顺便再说一下,这种带着一个continuation当参数传来传去的代码风格叫continuation passing style,也就是传说中的CPS。 将一段不带continuation的代码转换成continuation passing style的过程叫CPS变换。 (更准确的说,CPS变换是指将代码中的非尾调用转换成尾调用。)

简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation,布布扣,bubuko.com

简单易懂的程序语言入门小册子(6):基于文本替换的解释器,引入continuation

标签:com   http   blog   style   class   div   img   code   size   log   tar   

原文地址:http://www.cnblogs.com/skabyy/p/3695885.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!