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

时间:2022-09-20 21:28:28

或许在加入continuation之前要先讲讲费这么大劲做这个有什么意义。 毕竟用不用continuation的计算结果都是一样的。 不过,这是一个兴趣使然的系列,学习这些知识应该完全出于好奇与好玩的想法。 所以我才不会告诉你们通过控制continuation可以实现call-with-current-continuation和异常处理等功能呢。

我先简要描述一下加入continuation后解释器是怎么工作的。 加入continuation后的解释器是以迭代的方式工作的。 迭代的状态量有两个,第一个是一个待求值的表达式或者求到的值,第二个是Continuation。 假设解释器的输入是$M$,那么一开始的状态表示为$\left<M, {mt}\right>_v$。 第一个状态量是个表达式$M$,这时解释器开始对$M$求值。 这个过程用记号$\rightarrow_{v}$表示。 这个过程可能会将一些“下一步要做的事”保存到continuation。 求值到最后,状态会变为$\left<V, \kappa\right>_c$(如果没有无限循环)。 注意到下标变成c,$V$是一个值,所以不能再求值了,下一步要做的是从continuation $\kappa$中取出“下一步要做的事”执行。 这个过程也叫做“将continuation $\kappa$应用到值$V$”,用记号$\rightarrow_{c}$表示。 一个解释器的执行过程就是$\rightarrow_{v}$和$\rightarrow_{c}$交替运行,就好像太极里阴阳交替一样。 解释器执行到最后状态会变成$\left<V, {mt}\right>_c$,已求得值$V$,又没有“下一步要做的事”,也就是运行结束了,输出$V$。 说了这么多,本质上整个过程还是一句话:递归转迭代。顺便一提,加入continuation后的解释器叫做CK machine。

先列出目前为止的所有求值过程(call-by-value): \begin{equation*}\begin{array}{lcl}   eval(X) &=& X \\   eval(b) &=& b \\   eval(\lambda X.M) &=& \lambda X.M \\   eval((+ \; M \; N)) &=& eval(M) + eval(N) \\   eval((- \; M \; N)) &=& eval(M) - eval(N) \\   eval(({iszero} \; M)) &=& {true} \\                  && \text{其中} eval(M) = 0 \\   eval(({iszero} \; M)) &=& {false}, \\                  && \text{其中} eval(M) \neq 0 \\   eval((M \; N)) &=& eval(L[X \leftarrow eval(N)]) \\                  && \text{其中} eval(M) = \lambda X.L \\   eval(({fix} \; X_1 \; X_2 \; M)) &=& \lambda X_2.M[X_1 \leftarrow ({fix} \; X_1 \; X_2 \; M)] \end{array}\end{equation*} 下面分别介绍各个情况下如何加入continuation。

值与fix表达式

值是最简单的情况,直接应用continuation。 \begin{equation*}\begin{array}{lcl}   \left<X, \kappa\right>_v &\rightarrow_v& \left<X, \kappa\right>_c \\   \left<b, \kappa\right>_v &\rightarrow_v& \left<b, \kappa\right>_c \\   \left<\lambda X.M, \kappa\right>_v &\rightarrow_v& \left<\lambda X.M, \kappa\right>_c \\ \end{array}\end{equation*}

fix表达式和值的情况类似: \begin{equation*}\begin{array}{lcl}   \left<({fix} \; X_1 \; X_2 \; M), \kappa\right>_v &\rightarrow_v&     \left<\lambda X_2.M[X_1 \leftarrow ({fix} \; X_1 \; X_2 \; M)], \kappa\right>_c \end{array}\end{equation*}

基本运算

用$(o^n \; M_1 \; M_2 \; ... \; M_n)$表示基本运算,其中$o^n$是运算符,$M_1, ..., M_n$是参数,$n$是代表参数个数。 在我们的语言里目前有两个两参数的基本运算$o^2=\{+, -\}$和一个单参数的基本运算$o^1=\{{iszero}\}$。

计算基本运算前要先对所有参数求值。这里规定从左到右求值。 当解释器在求第$i$个参数$M_i$的值时,需要保存到continuation的数据有: 运算符$o^n$、 已求到的值$V_1, ..., V_{i-1}$(其中$V_1=eval(M), ..., V_{i-1}=eval(M_{i-1})$) 以及还没求值的参数$M_{i+1}, ..., M_n$。 因此,包含基本运算的continuation定义为: \begin{equation*}\begin{array}{lcl}   \kappa &=& {mt} \\          &|& \left<{opd}, \kappa, o^n, (V_1 \; ... \; V_{i-1}), (M_{i+1} \; ... \; M_n)\right> \end{array} \end{equation*}

求值过程为: \begin{equation*}\begin{array}{lcl}   \left<(o^n \; M_1 \; M_2 \; ... \; M_n), \kappa\right>_v &\rightarrow_v&     \left<M_1, \left<{opd}, \kappa, o^n, (M_2 \; ... \; M_n), ()\right>\right>_v \\   \left<V, \left<{opd}, \kappa, o^n, (M_{i+1} \; ... \; M_n), (V_1 \; ... \; V_{i-1})\right>\right>_c &\rightarrow_c&     \left<M_{i+1}, \left<{opd}, \kappa, o^n, (... \; M_n), (V_1 \; ... \; V_{i-1} V)\right>\right>_v \\   \left<V, \left<{opd}, \kappa, o^n, (), (V_1 \; ... \; V_{n-1})\right>\right>_c &\rightarrow_c&     \left<V', \kappa\right>_c \\   && \text{其中} V' = o^n(V_1, ..., V_{n-1}, V) \end{array}\end{equation*}

函数调用

函数调用$(M \; N)$先计算$M$的值,$N$保存到continuation: \begin{equation*}\begin{array}{lcl}   \left<(M \; N), \kappa\right>_v &\rightarrow_v& \left<M, \left<{arg}, \kappa, N\right>\right>_v \end{array}\end{equation*} $M$计算完后从continuation取出$N$计算(我们采用call-by-value的调用方式),同时保存$M$的计算结果$V$: \begin{equation*}\begin{array}{lcl}   \left<V, \left<{arg}, \kappa, N\right>\right>_c &\rightarrow_c& \left<N, \left<{fun}, \kappa, V\right>\right>_v \end{array}\end{equation*} $N$也计算完后,进行$\beta$归约: \begin{equation*}\begin{array}{lcl}   \left<V, \left<{fun}, \kappa, \lambda X.L\right>\right>_c &\rightarrow_c& \left<L[X \leftarrow V], \kappa\right>_v \end{array}\end{equation*} 这个地方很有意思,可以看到函数调用最后$\beta$归约的过程不会使continuation增长。 所有求值过程中,会导致continuation增长的几个过程是基本运算中对参数的计算过程、函数调用中对函数的计算过程以及对参数的计算过程。 一般也认为函数调用中函数的这个位置(也就是$(M \; N)$中$M$这个位置)也算参数位置, 所以判断一个函数调用是不是尾调用的依据是: 这个函数调用表达式是不是在一个参数位置,如果在参数位置,就不是尾调用。

上面分析的求值过程里增加了两种continuation: \begin{equation*}\begin{array}{lcl}   \kappa &=& ... \\          &|& \left<{arg}, \kappa, N\right> \\          &|& \left<{fun}, \kappa, V\right> \end{array} \end{equation*}

代码实现

函数value-of/k是$\rightarrow_v$。 函数apply-cont是$\rightarrow_c$。 编写代码要注意对value-of/和apply-cont的调用都必须是尾调用。

过程$\rightarrow_v$的代码:

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

出于写起来方便的原因,使用函数来保存continuation。 用(end-cont)表示空的continuation mt。 (end-cont)只有在程序结束的时候才会运行一次。 这里让(end-cont)打印了个">> Done!",这个打印只会运行一次。 如果打印了超过一次,或者没打印,那么代码肯定有错。

过程$\rightarrow_c$的代码:

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

寄存器风格的代码实现

这小节换个方式写代码。 求值过程有两个状态量:当前计算的表达式和当前的continuation。 我们用两个全局变量the-exp和the-cont来保存这两个状态量。 使用全局变量后,函数value-of/k和apply-cont就不再需要参数。 另外,还需要一个全局变量来保存下一步是要进行$\rightarrow_v$还是$\rightarrow_c$。 这个全局变量叫the-pc。 这三个全局变量被称作寄存器(所以叫寄存器风格)。 当the-pc的值为逻辑假#f时,解释器运行结束。 现在解释器运行时就是不断的执行the-pc直到the-pc的值是#f。 mainloop是这个主循环的过程:

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

进入主循环的过程前要先初始化寄存器:

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

过程value-of/k不再需要参数,用寄存器the-exp代替原来的exp1,the-cont代替原来的cont。 当然还有其他一些修改。代码如下:

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

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

过程apply-cont的修改和value-of/k类似。 代码如下:

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

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

思考

  1. 对宏展开过程translate和替换过程substitute做递归转迭代是否意义不大,为什么?
  2. 加入continuation后call-by-name的调用方式下的求值过程和代码实现?

简单易懂的程序语言入门小册子(7):基于文本替换的解释器,加入continuation,重构解释器的更多相关文章

  1. 简单易懂的程序语言入门小册子(3):基于文本替换的解释器,let表达式,布尔类型,if表达式

    let表达式 let表达式用来声明一个变量. 比如我们正在写一个模拟掷骰子游戏的程序. 一个骰子有6个面. 所以这个程序多次用到了6这个数字. 有一天,我们忽然改变主意,要玩12个面的骰子. 于是我们 ...

  2. 简单易懂的程序语言入门小册子(1):基于文本替换的解释器,lambda演算

    最近比较闲,打算整理一下之前学习的关于程序语言的知识.主要的内容其实就是一边设计程序语言一边写解释器实现它.这些知识基本上来自Programming Languages and Lambda Calc ...

  3. 简单易懂的程序语言入门小册子(1&period;5):基于文本替换的解释器,递归定义与lambda演算的一些额外说明

    这一篇接在第一篇lambda演算的后面.讲讲一些数学知识. 经常有些看似很容易理解的东西,一旦要描述得准确无误,就会变得极为麻烦. 软件工程里也有类似情况:20%的代码实现了核心功能,剩下80%的代码 ...

  4. 简单易懂的程序语言入门小册子(5):基于文本替换的解释器,递归,不动点,fix表达式,letrec表达式

    这个系列有个显著的特点,那就是标题越来越长.忽然发现今天是读书节,读书节多读书. ==下面是没有意义的一段话============================================== ...

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

    当我写到这里的时候,我自己都吃了一惊. 环境.存储这些比较让人耳熟的还没讲到,continuation先出来了. *里对continuation的翻译是“延续性”. 这翻译看着总有些违和感而且那 ...

  6. 简单易懂的程序语言入门小册子(4):基于文本替换的解释器,递归,如何构造递归函数,Y组合子

    递归.哦,递归. 递归在计算机科学中的重要性不言而喻. 递归就像女人,即令人烦恼,又无法抛弃. 先上个例子,这个例子里的函数double输入一个非负整数$n$,输出$2n$. \[ {double} ...

  7. Go语言入门篇-gRPC基于golang &amp&semi; java简单实现

    一.什么是RPC 1.简介: RPC:Remote Procedure Call,远程过程调用.简单来说就是两个进程之间的数据交互. 正常服务端的接口服务是提供给用户端(在Web开发中就是浏览器)或者 ...

  8. C语言入门(2)——安装VS2013开发环境并编写第一个C语言程序

    在C语言入门系列中,我们使用Visual studio 2013 Professional作为开发工具.本篇详细介绍如何安装Visualstudio 2013 Professional并写出我们第一个 ...

  9. 《Java从入门到失业》第一章:计算机基础知识(三):程序语言简介

    1.3程序语言简介 我们经常会听到一些名词:低级语言.高级语言.编译型.解释型.面向过程.面向对象等.这些到底是啥意思呢?在正式进入Java世界前,笔者也尝试简单的聊一聊这块东西. 1.3.1低级语言 ...

随机推荐

  1. Linux 下配置php开发环境

    windows下有一键安装的环境很方便,不过现实中常常服务器是linux系统.想要搭建环境怎么搞呢? 边学变发直播博客,不定期更新.

  2. webdynpro

    -------------------------------------------------------------------------------------WebDynpro For A ...

  3. jquery实现百度类似搜索提示功能(AJAX应用)

    有时候觉得百度那个输入内容就有提示的工具很神奇,它究竟是怎么做到的呢?以前在一个进销存系统中也做过这么个功能,但是远远不及百度的功能强大,百度可以输入首字母,关键字拼音,或关键字都可以匹配,有时在想, ...

  4. 关于HTML的编码问题

    平时我在写html文件时,很容易忘掉这个文件的编码类型,<meta charset=”utf-8”> 的语句,因为编辑器默认设置了一个编码,所以在我没有写编码格式设置语句的情况下,效果依然 ...

  5. C&num; 多线程通信详解

    一.WaitHandler的类层次 可以看到 WaitHandle是 事件(EventWaitHandle).互斥体(Mutex).信号量(Sempahore)的父类. WaitHandle我们最经常 ...

  6. SU Demo之02Filtering--01Sufilter

    欢迎各位网友批评指正! 今天博文例子位于如下目录: 第一个脚本: 下面是显示结果: 第二个脚本: 运行结果如下: 第三个脚本: 第四个脚本: 第五个脚本: 最后看看sumute命令的说明:

  7. Asp&period;net使用jQuery实现数据绑定与分页

    使用jQuery来实现Gridview, Repeater等服务器端数据展示控件的数据绑定和分页.本文的关注重点是数据如何实现数据绑定. Content jQuery的强大和可用性使得其迅速的流行起来 ...

  8. 推荐用于格式化以及高亮显示SQL文的PHP类-SqlFormatter

    有时候做开发用到SQL文的时候,由于sql文太长,很难真正看清楚这个sql文的结构. 所以需要一个工具能够自动对SQL文进行排版,在网上有幸找到这个用php写的类能处理这个任务. 原文地址是http: ...

  9. 实例讲解js正则表达式的使用

    前言:正则表达式(regular expression)反反复复学了多次,学了又忘,忘了又学,这次打算把基本的东西都整理出来,加强记忆,也方便下次查询. 学习正则表达式之前首先需要掌握记忆这些基本概念 ...

  10. BUCTOJ&lowbar;ACM2017C 回文串的热爱

    #include "iostream" #include "algorithm" #include "cstdio" #include &q ...