简单易懂的程序语言入门小册子(7):基于文本替换的解释器,加入continuation,重构解释器
或许在加入continuation之前要先讲讲费这么大劲做这个有什么意义。 毕竟用不用continuation的计算结果都是一样的。 不过,这是一个兴趣使然的系列,学习这些知识应该完全出于好奇与好玩的想法。 所以我才不会告诉你们通过控制continuation可以实现call-with-current-continuation和异常处理等功能呢。
我先简要描述一下加入continuation后解释器是怎么工作的。 加入continuation后的解释器是以迭代的方式工作的。 迭代的状态量有两个,第一个是一个待求值的表达式或者求到的值,第二个是Continuation。 假设解释器的输入是M,那么一开始的状态表示为⟨M,mt⟩v。 第一个状态量是个表达式M,这时解释器开始对M求值。 这个过程用记号→v表示。 这个过程可能会将一些“下一步要做的事”保存到continuation。 求值到最后,状态会变为⟨V,κ⟩c(如果没有无限循环)。 注意到下标变成c,V是一个值,所以不能再求值了,下一步要做的是从continuation κ中取出“下一步要做的事”执行。 这个过程也叫做“将continuation κ应用到值V”,用记号→c表示。 一个解释器的执行过程就是→v和→c交替运行,就好像太极里阴阳交替一样。 解释器执行到最后状态会变成⟨V,mt⟩c,已求得值V,又没有“下一步要做的事”,也就是运行结束了,输出V。 说了这么多,本质上整个过程还是一句话:递归转迭代。顺便一提,加入continuation后的解释器叫做CK machine。
先列出目前为止的所有求值过程(call-by-value): eval(X)=Xeval(b)=beval(λX.M)=λX.Meval((+MN))=eval(M)+eval(N)eval((−MN))=eval(M)−eval(N)eval((iszeroM))=true其中eval(M)=0eval((iszeroM))=false,其中eval(M)≠0eval((MN))=eval(L[X←eval(N)])其中eval(M)=λX.Leval((fixX1X2M))=λX2.M[X1←(fixX1X2M)]
值与fix表达式
值是最简单的情况,直接应用continuation。 ⟨X,κ⟩v→v⟨X,κ⟩c⟨b,κ⟩v→v⟨b,κ⟩c⟨λX.M,κ⟩v→v⟨λX.M,κ⟩c
fix表达式和值的情况类似: ⟨(fixX1X2M),κ⟩v→v⟨λX2.M[X1←(fixX1X2M)],κ⟩c
基本运算
用(onM1M2...Mn)表示基本运算,其中on是运算符,M1,...,Mn是参数,n是代表参数个数。 在我们的语言里目前有两个两参数的基本运算o2={+,−}和一个单参数的基本运算o1={iszero}。
计算基本运算前要先对所有参数求值。这里规定从左到右求值。 当解释器在求第i个参数Mi的值时,需要保存到continuation的数据有: 运算符on、 已求到的值V1,...,Vi−1(其中V1=eval(M),...,Vi−1=eval(Mi−1)) 以及还没求值的参数Mi+1,...,Mn。 因此,包含基本运算的continuation定义为: κ=mt|⟨opd,κ,on,(V1...Vi−1),(Mi+1...Mn)⟩
求值过程为: ⟨(onM1M2...Mn),κ⟩v→v⟨M1,⟨opd,κ,on,(M2...Mn),()⟩⟩v⟨V,⟨opd,κ,on,(Mi+1...Mn),(V1...Vi−1)⟩⟩c→c⟨Mi+1,⟨opd,κ,on,(...Mn),(V1...Vi−1V)⟩⟩v⟨V,⟨opd,κ,on,(),(V1...Vn−1)⟩⟩c→c⟨V′,κ⟩c其中V′=on(V1,...,Vn−1,V)
函数调用
函数调用(MN)先计算M的值,N保存到continuation: ⟨(MN),κ⟩v→v⟨M,⟨arg,κ,N⟩⟩v
上面分析的求值过程里增加了两种continuation: κ=...|⟨arg,κ,N⟩|⟨fun,κ,V⟩
代码实现
函数value-of/k是→v。 函数apply-cont是→c。 编写代码要注意对value-of/和apply-cont的调用都必须是尾调用。
过程→v的代码:
出于写起来方便的原因,使用函数来保存continuation。 用(end-cont)表示空的continuation mt。 (end-cont)只有在程序结束的时候才会运行一次。 这里让(end-cont)打印了个">> Done!",这个打印只会运行一次。 如果打印了超过一次,或者没打印,那么代码肯定有错。
过程→c的代码:
寄存器风格的代码实现
这小节换个方式写代码。 求值过程有两个状态量:当前计算的表达式和当前的continuation。 我们用两个全局变量the-exp和the-cont来保存这两个状态量。 使用全局变量后,函数value-of/k和apply-cont就不再需要参数。 另外,还需要一个全局变量来保存下一步是要进行→v还是→c。 这个全局变量叫the-pc。 这三个全局变量被称作寄存器(所以叫寄存器风格)。 当the-pc的值为逻辑假#f时,解释器运行结束。 现在解释器运行时就是不断的执行the-pc直到the-pc的值是#f。 mainloop是这个主循环的过程:
进入主循环的过程前要先初始化寄存器:
过程value-of/k不再需要参数,用寄存器the-exp代替原来的exp1,the-cont代替原来的cont。 当然还有其他一些修改。代码如下:
过程apply-cont的修改和value-of/k类似。 代码如下:
思考
- 对宏展开过程translate和替换过程substitute做递归转迭代是否意义不大,为什么?
- 加入continuation后call-by-name的调用方式下的求值过程和代码实现?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人