最近比较闲,打算整理一下之前学习的关于程序语言的知识。主要的内容其实就是一边设计程序语言一边写解释器实现它。这些知识基本上来自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. linux中一些简便的命令之wc

    wc命令是统计文本中的字符数.单词数以及文本行数的,具体参数如下: -l 统计文本中的行数 -w 统计文本中的单词数 -c/m 统计文本中的字符数 -L 统计文本中最长行的字符数 当然使用时也可以不带 ...

  2. Java中IO流中的装饰设计模式(BufferReader的原理)

    本文粗略的介绍下JavaIO的整体框架,重在解释BufferReader/BufferWriter的演变过程和原理(对应的设计模式) 一.JavaIO的简介 流按操作数据分为两种:字节流与字符流. 流 ...

  3. interface21 - web - ContextLoaderListener(Spring Web Application Context加载流程)

    前言 最近打算花点时间好好看看spring的源码,然而现在Spring的源码经过迭代的版本太多了,比较庞大,看起来比较累,所以准备从最初的版本(interface21)开始入手,仅用于学习,理解其设计 ...

  4. Java工程师学习指南 初级篇

    Java工程师学习指南 初级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...

  5. 第一个Quartz程序 (二)

    1 我们使用maven项目 2 创建一个job类,在execute()方法里写上业务逻辑代码. 3 在另外一个类中创建触发器,调度器,并且绑定job. 首先在项目的pom.xml引入需要的jar包. ...

  6. Ubuntu下将现有的文件打包成deb包

    转自:http://www.linuxidc.com/Linux/2008-04/12297.htm deb是Debian Linux的软件包格式.一般来说是需要通过编译源码然后制作deb包,今天由于 ...

  7. SQL Where in (1,2,3,4) 换成字段一列的值

    ) ; , ) ) FROM r_resource WHERE id IN ( @resource) 换成 ) : , ) ) FROM r_resource )) SELECT cid,id FRO ...

  8. IdentityServer4关于多客户端和API的最佳实践【含多类型客户端和API资源,以及客户端分组实践】【下】

    经过前两篇文章你已经知道了关于服务器搭建和客户端接入相关的基本资料,本文主要讲述整个授权系统所服务的对象,以ProtectApi资源为演示 目标: 1)实现多资源服务器针对请求的token校验,接入I ...

  9. Java坦克大战(一)

    接下来的几篇博客,想记录一下通过学习坦克大战项目来循序渐进的学习Java基础.主要是为了巩固基础知识,当然学习编程重要的还是多敲,问题通常是在敲代码的过程中发现的,积累也是在敲代码中寻求的经验.这个坦 ...

  10. C#中的out、ref、params详解

    out参数: 如果你在一个方法中,返回多个相同类型的值的时候,可以考虑返回一个数组.但是,如果返回多个不同类型的值的时候,返回数组就不行了,那么这个时候,我们可以考虑使用out参数.out参数就侧重于 ...