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

我还记得高中奥数竞赛培训时的老师这样说过:“解题时一定要抓住定义。” 编程和解题一样,也要抓住定义。 所以在写解释器前,得先定义好这门要解释的程序语言。 这门程序语言基于Lambda演算。

从\(\lambda\)演算讲起

真不想讲\(\lambda\)演算……算了,还是简要说明一下。\(\lambda\)演算之于程序语言中的地位好比集合论之于数学。正如每一本数学教材,都要从集合论开始; 每一本程序语言教材,也要从\(\lambda\)演算讲起。 不过话说回来,追根溯源\(\lambda\)演算也是从集合论搭起来。 咱就不走那么远了,又累又没什么意思……

\(\lambda\)演算中的基本类型只有变量和函数两种。 变量用大写字母\(X\)表示。 像\(a,b,x,y,abc,...\)都是变量。 一个函数包含两个元素: 一个是函数参数(形参),它是一个变量; 另一个元素是函数体,它是一个\(\lambda\)演算表达式(这里是递归定义)。 用(lambda X M)表示一个函数, 其中X是一个变量,M是一个\(\lambda\)演算表达式( 别吐槽参数X那里少了个括号。 )。 为了描述的简洁,也用\(\lambda X.M\)表示一个函数。

举个例子,\(\lambda x.x\)是一个恒等函数\(f(x) = x\)。 在数学上一般用\(f(a)\)表示函数调用,\(a\)是实参。 在\(\lambda\)演算中把函数也放入括号,记为\((\lambda x.x \; a)\)。 函数调用的计算方法是在函数体中用实参替换形参。 在这个例子里\((\lambda x.x \; a) = a\)。 这个计算过程称为归约。

\(\lambda\)演算的函数都只包含一个参数。 如果要使用多参函数,可以用多个函数嵌套。 下面是一个例子: \[ \lambda x.\lambda y.(x \; y) \] 这种技巧被称作currying。

从上面的讨论看出,\(\lambda\)演算只包含三种表达式。 形式化地定义\(\lambda\)演算的语法如下: \begin{eqnarray*}   M, N, L &=& X \\           &|& \lambda X.M \\           &|& (M \; N) \end{eqnarray*} 这里用大写字母\(M\)、\(N\)和\(L\)代表\(\lambda\)演算的表达式, 这是个递归定义,第二行、第三行出现了\(M\)和\(N\)。 第三行表达式是一个函数调用,一般要求处于函数位置的\(M\)应该要能归约成一个函数,否则归约就没法进行下去啦。

下面给出几个\(\lambda\)演算的表达式的例子: \begin{eqnarray*}   & x \\   & \lambda x.x \\   & (\lambda x.x \; y) \\   & (\lambda x.(x \; x) \; \lambda x.x) \\   & (\lambda x.(x \; x) \; \lambda x.(x \; x)) \end{eqnarray*}

\(\lambda\)演算的归约依赖于替换操作。 在介绍替换操作之前还得先介绍自由变量。

自由变量

考察一个表达式:\((\lambda x.(\lambda x.x \; x) \; a)\)。 这个表达式归约到\((\lambda x.x \; a)\)。 可以看到,在\(\lambda x.(\lambda x.x \; x)\)函数体\((\lambda x.x \; x)\)中参数位置的变量\(x\)和\(\lambda x.x\)中点后面的\(x\)是不一样的。 参数位置中的\(x\)被替换成\(a\),而\(\lambda x.x\)中点后面的\(x\)没有被替换。 被替换的\(x\)称为表达式\((\lambda x.x \; x)\)的自由变量。 在函数调用的替换过程中只有自由变量会被替换。

自由变量指一个表达式中没有受到约束的变量。 约束指这个变量不是作为某个函数的参数而存在。 如表达式\(\lambda x.(f x)\)中\(f\)是自由变量,\(x\)不是自由变量。 用\(FV(M)\)表示表达式\(M\)中的所有自由变量的集合。

从这里开始,描述和\(\lambda\)演算有关的一些定义和算法将遵循\(\lambda\)演算的语法定义。 所以计算\(FV(M)\)的算法(也是\(FV(M)\)的精确定义)应该分成变量、函数和函数调用三种情况讨论: \begin{eqnarray*}   FV(X) &=& \{X\} \\   FV(\lambda X.M) &=& FV(M) \backslash \{X\} \\   FV((M \; N)) &=& FV(M) \cup FV(N) \end{eqnarray*}

替换

用记号\(M[X \leftarrow N]\)表示在表达式\(M\)中将自由变量\(X\)(如果有出现这个自由变量)替换成表达式\(N\)。 更准确的定义如以下公式: \begin{eqnarray*}   X_1[X_1 \leftarrow N] &=& N \\   X_2[X_1 \leftarrow N] &=& X_2 \\   &&其中X_1 \neq X_2 \\   (\lambda X_1.M)[X_1 \leftarrow N] &=& (\lambda X_1.M) \\   (\lambda X_1.M)[X_2 \leftarrow N] &=& (\lambda X_3.M[X_1 \leftarrow X_3][X_2 \leftarrow N]) \\   &&其中X_1 \neq X_2, X_3 \notin FV(N), X_3 \notin FV(M)\backslash\{X_1\} \\   (M_1 \; M_2)[X \leftarrow N] &=& (M_1[X \leftarrow N] \; M_2[X \leftarrow N]) \end{eqnarray*} 第四个公式看着比较复杂,其实是为了避免\(N\)中有自由变量\(X_1\)这种情况。 举个例子,\(\lambda x.y[y \leftarrow (x x)]\)应该替换为\(\lambda z.(x x)\)。 如果替换成\(\lambda x.(x x)\)就不对了。

如果\(N\)中没有自由变量\(X_1\),那么这个公式可以简化成: \begin{eqnarray*}   (\lambda X_1.M)[X_2 \leftarrow N] = (\lambda X_1.M[X_2 \leftarrow N]) \end{eqnarray*}

归约

所谓归约,可以理解成求值,或者表达式化简(初中好像有学过代数表达式化简)。 \(\lambda\)演算有三种归约方法。 三种归约分别称为\(\alpha\)归约,\(\beta\)归约和\(\eta\)归约。 名字看着很渗人,不表示这三种归约难以理解,只说明命名的人没有一颗爱玩的心。

  • \(\alpha\)归约的意思是,函数参数变量的变量名是什么无关紧要。 比如\(\lambda x.x\)和\(\lambda y.y\)表示的同一个函数。 这个归约很基本,但是几乎上不会被用到就是的了。 \[ \lambda X_1.M \rightarrow_\alpha \lambda X_2.M[X_1 \leftarrow X_2] \quad \text{其中}X_2 \notin FV(M)\]
  • \(\beta\)归约表示了函数调用过程,是最常用的归约。 \(\beta\)归约用函数调用的输入参数(实参)替换函数体中出现的参数变量(形参): \[ (\lambda X.M \; N) \rightarrow_\beta M[X \leftarrow N] \]
  • \(\eta\)归约指: \[ \lambda X.(M \; X) \rightarrow_\eta M \quad \text{其中}X \notin FV(M)\] 这个有点怪,但仔细想想不难理解。

一个解释器的作用是输入一个表达式,输出该表达式归约到最简(不能再\(\beta\)归约)的形式。 一般我们是希望这个最简形式能够是一个变量(\(X\))或者一个函数(\(\lambda X.M\)),因为函数调用是用来让人进行\(\beta\)归约的。 变量,或者函数,被称为“值”。 但是也有些坏掉了的表达式像\((x \; x)\),由于\(x\)是个变量而非函数,这个表达式没法再归约。 通常这种表达式被认为非法的表达式。 如果输出这种结果就表示输入程序有误,程序崩溃。 另外有些表达式不能归约到某种最简形式,也就是无限循环(可怜的西西弗斯)。 无限循环的一个经典例子是这个输入:\((\lambda x.(x \; x) \; \lambda x.(x \; x))\)。

一个解释器,给它一个输入,它会有以下三种情况:

  • 输出一个值:-)
  • 崩溃XD
  • 无限循环@_@

呼!总算写完。

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

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

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

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

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

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

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

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

    或许在加入continuation之前要先讲讲费这么大劲做这个有什么意义. 毕竟用不用continuation的计算结果都是一样的. 不过,这是一个兴趣使然的系列,学习这些知识应该完全出于好奇与好玩的 ...

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

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

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

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

  7. Go语言入门篇-gRPC基于golang & 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. “玲珑杯”ACM比赛 Round #18---图论你先敲完模板(DP+思维)

    题目链接 DESCRIPTION INPUT OUTPUT SAMPLE INPUT 2 3 2 3 5 7 3 10 3 5 7 SAMPLE OUTPUT 12 26 HINT 官方题解: 代码如 ...

  2. LDA-线性判别分析(二)Two-classes 情形的数学推导

    本来是要调研 Latent Dirichlet Allocation 的那个 LDA 的, 没想到查到很多关于 Linear Discriminant Analysis 这个 LDA 的资料.初步看了 ...

  3. 全网最详细的Eclipse里如何正确新建普通的Java web项目并发布到Tomcat上运行成功【博主强烈推荐】(图文详解)

    不多说,直接上干货! 首先,大家要明确,IDEA.Eclipse和MyEclipse等编辑器之间的新建和运行手法是不一样的. 如果是在Myeclipse里,则是File -> new -> ...

  4. 七:理解控件的运行机制(例:基于CompositeControl命名空间的控件)

    组合控件与WebControl控件的事件和属性相差不大组合控件,顾名思义就是把一些控件组合起来形成一个控件这个控件将包含这些控件称为他的子控件 CompositeControl类实现了INameCon ...

  5. [Golang] GoConvey测试框架使用指南

    GoConvey 是一款针对Golang的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性. GoConvey 网站 : http://smartystreet ...

  6. 西门子PLC SCL语言开发学习笔记(二)

    今天来讲下scl两个关键的点 一.按键事件 比如地址I0.0是某个按钮的状态,他只有True和False两个状态,所以我们要获得按下事件需要我们自己模拟. #KeyPress := "Btn ...

  7. bus总线

    有时候两个组件也需要通信(非父子关系).当然Vue2.0提供了Vuex,但在简单的场景下,可以使用一个空的Vue实例作为中央事件总线. eventBus.js放在assets import Vue f ...

  8. FFmpeg时间戳详解

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584910.html 1. I帧/P帧/B帧 I帧:I帧(Intra-coded pi ...

  9. macOS 系统 .DS_Store 文件详解

    .DS_Store 文件 .DS_Store 是 Finder 用来存储这个文件夹的显示属性的:比如文件图标的摆放位置. 显示/隐藏 Mac 隐藏文件 显示:defaults write com.ap ...

  10. Abp中自定义Exception的HttpStatusCode

    Abp中在新版本中,抛出的异常(比如:UserFriendlyException)通过AjaxResponse封装后返回的时候,HttpStatusCode默认指定成了500. 对于一些默认封装好的处 ...