7 行代码 3 分钟:从零开始实现一门编程语言

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本文介绍了多种解释器实现。通过修改最后一个解释器,你应该可以快速测试关于编程语言的新想法。如果你希望有一种语法不一样的语言,就可以构建一个解析器,把 s-表达式转储。这样,你就可以干净利落地将语法设计与语义设计分开。

本文最初发布于 Matt Might 的个人博客。

本文介绍了多种解释器实现。通过修改最后一个解释器,你应该可以快速测试关于编程语言的新想法。如果你希望有一种语法不一样的语言,就可以构建一个解析器,把 s-表达式转储。这样,你就可以干净利落地将语法设计与语义设计分开。

实现一门编程语言是任何程序员都不应该错过的经验;这个过程可以培养你对计算的深刻理解,而且很有趣。

本文直击本质,把整个过程归结为:一个面向函数式(但图灵等价)编程语言的 7 行解释器,而其实现只需要大约 3 分钟。

这个 7 行的解释器展示了许多解释器中都存在的可扩展架构——《计算机程序的结构与解释》中的 eval/apply 设计模式:

本文中总共有三种语言的实现:

一个使用 Scheme 耗时 3 分钟实现的 7 行解释器;

使用Racket重新实现;

一个耗时“一下午”实现的 100 行解释器,实现了顶层绑定形式、显式递归、副作用、高阶函数等功能。如果想要实现一门功能更丰富的语言,那么最后一个解释器是一个不错的起点。

一门小语言(但图灵等价)

最容易实现的编程语言是一种极简的高阶函数式编程语言,名为λ演算(lambda calculus)。

实际上,λ演算是所有主要的函数式语言的核心——Haskell、Scheme 和 ML——但它也存在于 JavaScript、Python 和 Ruby 中。它甚至隐藏在 Java 中,不知道你是否知道在哪里可以找到它。

λ演算简史

阿隆佐·丘奇在 1929 年开发了λ演算。

那时,它还不叫编程语言,因为当时没有计算机;没有什么东西可以“编程”。

它实际上只是一个用于函数推理的数学符号。幸运的是,阿隆佐·丘奇有一个博士生叫艾伦·图灵。

艾伦·图灵定义了图灵机,这成为通用计算机第一个公认的定义。

人们很快发现,λ演算和图灵机是等价的:任何能用λ演算描述的函数都能在图灵机上实现,而任何能在图灵机上实现的函数都能用λ演算描述。

值得注意的是,λ演算中只有三种表达式:变量引用、匿名函数和函数调用。

匿名函数

匿名函数的编写采用“lambda-dot”标记法,如下所示:

image.png

该函数接受参数v ,返回值e 。如果用 JavaScript 编写,上述代码等价于:

image.png

函数调用

函数调用的写法是使两个表达式相邻:

image.png

JavaScript(或其他任何语言)的写法如下:

image.png

示例

将参数原样返回的恒等函数写法如下:

image.png

我们可以将恒等函数应用于恒等函数:

image.png

(返回当然也是恒等函数。)下面这个程序更有意思一些:

image.png

你能搞懂它做了什么吗?

这到底是怎样的一种“编程”语言?

乍一看,这门简单的语言似乎缺少递归和迭代,更不用说数值、布尔、条件、数据结构等其他东西。这种语言怎么可能是通用的呢?

λ演算达到图灵等价是通过两个最酷的编程黑科技实现的:Church 编码和 Y 组合子。

关于 Y 组合子,我已经写过一篇文章,关于Church编码,也写过一篇。不过,你不想读这些文章也没事,我只需一个程序就可以说服你,λ演算的功能远超你的预期:

image.png

这个看上去无害的程序名为 Omega,如果你试图执行它,就发现它不会终止!(看看你能不能找出原因)。

实现λ演算

下面是用 R5RS Scheme 耗时 3 分钟实现的一个 7 行λ演算解释器。从技术上讲(下文有解释),它是一个基于环境的指示型解释器。

image.png

这段代码将从 stdin 读取一个程序,解析它,求值并打印结果。(去掉注释和空行,它只有 7 行)。Scheme 的read函数简化了词法分析和解析——只要你愿意生活在“平衡圆括号”(即s-表达式)的语法世界中。(如果不愿意,你就必须仔细研究解析中的词法分析;可以从我的一篇关于词法分析的文章入手)。在 Scheme 中,read从 stdin 中获取括号括起来的输入,并将其解析为一棵树。

eval 和apply 两个函数构成了解释器的核心。尽管是在 Scheme 中,但我们可以给予这些函数概念上的“签名”:

image.png

eval函数接收一个表达式和一个环境然后转换为一个值。表达式可以是一个变量,一个 lambda 项或一个应用程序。环境是一个从变量到值的映射,用来定义一个开项的自由变量。(开项是一个变量的非绑定出现。)例如,考虑一下表达式(λ x . z)。这个项是开放的,因为我们不知道z是什么。

由于用的是 R5RS Scheme,我们可以使用关联列表来定义环境。

闭包是一个函数的编码,它将一个(可能是开放的)lambda 表达式与一个环境配对,以定义其自由变量。换句话说,一个闭包封闭了一个开项。

使用 Racket 的实现更简洁

Racket是 Scheme 的一种方言,它功能齐备,可以把事情做好。Racket 提供了一个可以清理解释器的匹配结构,如下所示:

image.png

这个代码多点,但更简洁,更容易理解。

一门更大的语言

λ演算是一门很小的语言。即便如此,其解释器的 eval/apply 设计也可以扩展到更大的语言。例如,用大约 100 行代码,我们可以为一个相当大的 Scheme 子集实现一个解释器。

考虑一种具有各种表达形式的语言:

变量引用,如:x、foo、save-file。

数值和布尔常量,如:300、3.14、#f。

基本操作,如:+、-、<=。

条件:(if condition if-true if-false)。

变量绑定:(let ((var value) ...) body-expr)。

递归绑定:(letrec ((var value) ...) body-expr)。

变量可变:(set! var value)。

定序:(begin do-this then-this)。现在,为这门语言添加 3 个顶层形式:

函数定义:(define (proc-name var ...) expr)。

全局定义:(define var expr)。

顶层表达式:expr。下面是完整的解释器,其中包括测试工具和测试用例:

image.png

image.png

image.png

image.png

image.png

image.png

下载源代码,请点击https://matt.might.net/articles/implementing-a-programming-language/minilang.rkt?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2NTU0NTMzMzAsImZpbGVHVUlEIjoibG9xZVcyRXl2d0hkSkxBbiIsImlhdCI6MTY1NTQ1MzAzMCwidXNlcklkIjoyMDQxOTA5MH0.Nv5UyUdCUJNT7c0kIaPSE0g0f4k9Ed26rLl2Bu5RpG4

结语

通过修改最后一个解释器,你应该可以快速测试关于编程语言的新想法。

如果你希望有一种语法不一样的语言,就可以构建一个解析器,把 s-表达式转储。这样,你就可以干净利落地将语法设计与语义设计分开。

查看英文原文:

https://matt.might.net/articles/implementing-a-programming-language?accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2NTU0NTMzMzAsImZpbGVHVUlEIjoibG9xZVcyRXl2d0hkSkxBbiIsImlhdCI6MTY1NTQ1MzAzMCwidXNlcklkIjoyMDQxOTA5MH0.Nv5UyUdCUJNT7c0kIaPSE0g0f4k9Ed26rLl2Bu5RpG4

目录
相关文章
|
5月前
|
JavaScript 前端开发 Java
IT入门知识第二部分《编程语言》(2/10)
IT入门知识第二部分《编程语言》(2/10)
42 0
|
5月前
|
机器学习/深度学习 人工智能 前端开发
哪个编程语言更适合初学者并能快速掌握?
【7月更文挑战第2天】哪个编程语言更适合初学者并能快速掌握?
150 56
让我设计一门编程语言或开发一套解决框架
让我设计一门编程语言或开发一套解决框架
95 2
|
数据采集 机器学习/深度学习 数据挖掘
入手一门编程语言,一起初识Python
入手一门编程语言,一起初识Python
107 0
|
XML Java 程序员
python编程语言
python编程语言
174 0
|
人工智能 前端开发 搜索推荐
程序初学者推荐学习的三种热门编程语言
在当前的社会需求中,市场上运用最多的、最为广泛的、最热门的、最常用的编程语言可以大致分为一下三种:C语言、JAVA语言、Python语言。
|
Java 程序员 编译器
Yin 语言:学习设计和实现一门编程语言
大多数语言没能吸取历史教训 大多数语言受到宗教性的推崇,拥有一个过于狂热的社区,因此难以改正自己的错误 有些语言为程序员做得太少,有些语言为程序员做得太多 有些语言提供了太少的抽象,有些语言提供了太多的抽象 有些语言太不顾及可用性,游戏语言过于重视可用性而忽视了可用性之外的东西 有些语言从数学和逻辑那里学得太少,有些语言学得太多 有些语言太不顾及类型,有些语言对类型考虑过多
589 0
Yin 语言:学习设计和实现一门编程语言
|
设计模式 算法 程序员
【译】需要学习的是编程,而不是编程语言
我们不仅是程序员,而且是个(与时俱进的)学习者。鲜见的是有多少人认为他们是在学习编程的呢。
|
XML 存储 Java
不熟悉的编程语言,项目如何开展?
引言 公司中的开发一般是沿着一种核心开发语言如Java、C/C++、PHP进行相关开发。但由于产品新需求、项目新需要,免不了会使用自己不擅长的语言开发。甚至,现在全栈工程师也比比皆是。对于经验不丰富的职场人,如何开展工作呢? 结合我近期的项目经历,我说下我的经验和教训。
206 0
不熟悉的编程语言,项目如何开展?
|
Java PHP C语言
编程语言基础知识详细总结之数组,编程知识点你必要掌握(十二)
  学好编程从基础开始,下面是总结的关于编程的一些小知识,如果你也喜欢编程,那就加入我们吧,持续分享c语言,java,php,html等编程的小知识,欢迎关注趣IT科技。   数组: 存放的类型是一致的。多个数组元素的地址是连续的。   一维数组的初始化:   int a[5]={1,2,3,4,5}; 合法   int a[5]={1,2,3, }; 合法   int a[]={1,2,3,4,5}; 合法,常考,后面决定前面的大小!   int a[5]={1,2,3,4,5,6}; 不合法,赋值的个数多余数组的个数了
126 0