Loading web-font TeX/Math/Italic

简单易懂的程序语言入门小册子(2):基于文本替换的解释器,加入整数类型

为了有条不紊地实现一个解释器,我将按以下三个步骤走:

  1. 明确语法 
  2. 针对语法描述求值过程  
  3. 根据求值过程编写代码实现

语法

λ演算不适合作为一门实际使用的程序语言。 λ演算只有变量和函数两种类型,而其他常用类型如整数、布尔、字符等都没有。 虽然可以通过编码的方式表示这些常用类型,但这样也很麻烦。 通常直接扩展λ演算,加入一些常用类型以及针对这些类型的基本运算。 这种扩展后的语言简称为ISWIM,全称未知……

为简单起见,我只加入整数类型,以及加法和减法。 扩展后的语法如下: M,N,L=X|b|λX.M|(+MN)|(MN)|(MN)

新加入的第二行b表示一个整数, 第四行是一个加法运算的表达式, 第五行是一个减法运算的表达式。

求值过程

为了描述一个计算这门语言的解释器的求值过程,首先要明确求值停止条件。 我们规定当归约到XbλX.M这三种表达式之一时,认为解释器已经求出了最终结果。 这三种表达式称为值,用字母V表示。 V=X|b|λX.M

用记号eval(M)表示表达式M的求值结果。 对于值,求值只需返回它们自身。 eval(X)=Xeval(b)=beval(λX.M)=λX.M

加减法和函数调用这三行是递归定义,所以求值过程也是递归的。 eval((+MN))=eval((+V1V2))eval((MN))=eval((V1V2))eval((MN))=eval((V1V2))
其中V1=eval(M)V2=eval(N)

为了让解释器尽量简单,假设输入的程序是正确的。 也就是说,对于加减法运算的V1V2都是整数,记为b1b2; 函数调用里的V1是一个函数λX.L

加减法如字面本意,就是作加减法。 函数调用过程是一个β归约过程。 eval((+b1b2))=b1+b2eval((b1b2))=b1b2eval((λX.LV2))=eval(L[XV2])

由于加入了新的语法,替换过程也要添加相应的过程。 这里列上整个替换过程: X1[X1N]=NX2[X1N]=X2X1X2b[XN]=b(λX1.M)[X1N]=(λX1.M)(λX1.M)[X2N]=(λX3.M[X1X3][X2N])X1X2,X3FV(N),X3FV(M){X1}(+M1M2)[XN]=(+M1[XN]M2[XN])(M1M2)[XN]=(M1[XN]M2[XN])(M1M2)[XN]=(M1[XN]M2[XN])

最后总结求值过程如下: eval(X)=Xeval(b)=beval(λX.M)=λX.Meval((+MN))=eval(M)+eval(N)eval((MN))=eval(M)eval(N)eval((MN))=eval(L[Xeval(N)])eval(M)=λX.L

实现

这里使用Racket语言来编写解释器。 解释器输入不使用字符串,而是用Racket的符号系统。 使用符号系统是为了简化语法分析的工作。 利用Racket的模式匹配可以方便地实现语法分析。 另外,计算机输入λ还是很麻烦的,所以在具体实现的语言中用(lambda X M)代替λX.M

解释器是一个实现了eval函数的程序。 代码是求值过程的公式逐句转换,就不一一解释了。 value-of是求值过程:

求值1

求值2

substitute是替换过程:

替换1

替换2

在替换过程中有一处需要生成新变量(new-tmp-var)。 新变量不能和被代入的表达式中的自由变量重名。 一个选取新变量的方法就是选择程序里肯定不会出现的变量名。 我假定输入的程序没有以井号“#”开头的变量。 新生成的就以井号加数字的方式命名:#1, #2, #3,...。

临时变量

测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'a
>> 'a
 
12
>> 12
 
'(+ 12 13)
>> 25
 
'(- 32 23)
>> 9
 
'(lambda x (+ x 1))
>> '(lambda x (+ x 1))
 
'((lambda x (- x 1)) 22)
>> 21
 
'(((lambda x x) (lambda y y)) 11)
>> 11
 
'(((lambda x (lambda y x)) y) 0)
>> 'y

惰性求值

惰性求值指对一个表达式,只有在需要它的计算结果时才对它求值。

对于函数调用的求值过程,参数N可以先不进行求值: eval((MN))=eval(L[XN])eval(M)=λX.L

这种函数调用的求值方式就叫做call-by-name。 Call-by-name是一种惰性求值的调用方式。

在函数调用过程中先对参数求值的调用方式叫做call-by-value。 下面用例子展示这两种调用方式的不同。

Call-by-value: (λx.(+xx)(+23))(λx.(+xx)5)(+55)10

Call-by-name: (λx.(+xx)(+23))(+(+23)(+23))(+55)10

Call-by-name的过程中(+23)被计算了两次。 为了避免重复计算,有另一种同为惰性求值的调用方式叫call-by-need。 Call-by-need以后再介绍。

在两者都能成功求值的情况下,call-by-name的求值结果和call-by-value的求值结果是一样的。 它们的区别在于两者的求值过程不同。 看下面这个表达式: ((λy.λx.x(λx.(xx)λx.(xx)))(+1221))

如果用call-by-value的方式求值,必然要先求(λx.(xx)λx.(xx))的值。 而(λx.(xx)λx.(xx))是个无限循环。 所以call-by-value的调用方式会陷入死循环。

如果用call-by-name的方式求值,由于函数λy.λx.x中的函数体其实没涉及到y的, 所以(λx.(xx)λx.(xx))这个参数就函数调用过程后默默地消失了: ((λy.λx.x(λx.(xx)λx.(xx)))(+1221))(λx.x(+1221))(+1221)33

Call-by-name的代码实现只需在原来的基础上改一行:

callbyname

posted @   古霜卡比  阅读(1506)  评论(2编辑  收藏  举报
编辑推荐:
· 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训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示