百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

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

yund56 2025-02-26 11:51 5 浏览

本文最初发布于 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”标记法,如下所示:


 (λ v . e)

复制代码


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




 function (v) { return e ; } 

复制代码

函数调用

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




 (f e)

复制代码


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




 f(e)

复制代码

示例

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




 (λ x . x)

复制代码


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




 ((λ x . x) (λ a . a))

复制代码


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




 (((λ f . (λ x . (f x))) (λ a . a)) (λ b . b))

复制代码


你能搞懂它做了什么吗?

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

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


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


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




 ((λ f . (f f)) (λ f . (f f)))

复制代码


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

实现λ演算

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




; eval将一个表达式和一个环境转换成一个值
(define (eval e env) (cond
  ((symbol? e)       (cadr (assq e env)))
  ((eq? (car e) 'λ)  (cons e env))
  (else              (apply (eval (car e) env) (eval (cadr e) env)))))


; apply将一个函数和一个参数转换成一个值
(define (apply f x)
  (eval (cddr (car f)) (cons (list (cadr (car f)) x) (cdr f))))


; 从stdin读取并解析,然后求值:
(display (eval (read) '())) (newline)

复制代码


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


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




 eval  : Expression * Environment -> Value
 apply : Value * Value -> Value


 Environment = Variable -> Value
 Value       = Closure
 Closure     = Lambda * Environment

复制代码


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


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


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

使用 Racket 的实现更简洁

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




#racket语言


; 引入匹配库:
(require racket/match)


; eval匹配表达式类型:
(define (eval exp env) (match exp
  [`(,f ,e)        (apply (eval f env) (eval e env))]
  [`(λ ,v . ,e)   `(closure ,exp ,env)]
  [(? symbol?)     (cadr (assq exp env))]))


; apply用一个匹配来析构函数:
(define (apply f x) (match f
  [`(closure (λ ,v . ,body) ,env)
    (eval body (cons `(,v ,x) env))]))


; 读入、解析、求值:
(display (eval (read) '()))    (newline)

复制代码


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

一门更大的语言

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


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


  1. 变量引用,如:xfoosave-file
  2. 数值和布尔常量,如:3003.14#f
  3. 基本操作,如:+-<=
  4. 条件:(if condition if-true if-false)
  5. 变量绑定:(let ((var value) ...) body-expr)
  6. 递归绑定:(letrec ((var value) ...) body-expr)
  7. 变量可变:(set! var value)
  8. 定序:(begin do-this then-this)。现在,为这门语言添加 3 个顶层形式:
  9. 函数定义:(define (proc-name var ...) expr)
  10. 全局定义:(define var expr)
  11. 顶层表达式:expr。下面是完整的解释器,其中包括测试工具和测试用例:




#语言racket


(require racket/match)


;; 计算在eval和apply之间切换。


; eval分派表达式类型:
(define (eval exp env)
  (match exp
    [(? symbol?)          (env-lookup env exp)]
    [(? number?)          exp]
    [(? boolean?)         exp]
    [`(if ,ec ,et ,ef)    (if (eval ec env)
                              (eval et env)
                              (eval ef env))]
    [`(letrec ,binds ,eb) (eval-letrec binds eb env)]
    [`(let    ,binds ,eb) (eval-let binds eb env)]
    [`(lambda ,vs ,e)    `(closure ,exp ,env)]
    [`(set! ,v ,e)        (env-set! env v e)]
    [`(begin ,e1 ,e2)     (begin (eval e1 env)
                                 (eval e2 env))]
    [`(,f . ,args)        (apply-proc
                           (eval f env) 
                           (map (eval-with env) args))]))


; 一个方便的Currying eval的封装器:
(define (eval-with env) 
  (lambda (exp) (eval exp env)))


; eval for letrec:
(define (eval-letrec bindings body env)
  (let* ((vars (map car bindings))
         (exps (map cadr bindings))
         (fs   (map (lambda _ #f) bindings))
         (env* (env-extend* env vars fs))
         (vals (map (eval-with env*) exps)))
    (env-set!* env* vars vals)
    (eval body env*)))


; eval for let:
(define (eval-let bindings body env)
  (let* ((vars (map car bindings))
         (exps (map cadr bindings))
         (vals (map (eval-with env) exps))
         (env* (env-extend* env vars vals)))
    (eval body env*)))
    
; 将一个过程作用于参数:
(define (apply-proc f values) 
  (match f
    [`(closure (lambda ,vs ,body) ,env) 
     ; =>
     (eval body (env-extend* env vs values))]
    
    [`(primitive ,p)
     ; =>
     (apply p values)]))


;; 环境将变量映射到包含值的可变单元格。


(define-struct cell ([value #:mutable]))


; 清空环境:
(define (env-empty)  (hash))


; 初始化环境,绑定基本操作:
(define (env-initial)
  (env-extend* 
   (env-empty)
   '(+  -  /  *  <=  void  display  newline)
   (map (lambda (s) (list 'primitive s))
   `(,+ ,- ,/ ,* ,<= ,void ,display ,newline))))


; 查找一个值:
(define (env-lookup env var)
  (cell-value (hash-ref env var)))


; 在环境中设置一个值:
(define (env-set! env var value)
  (set-cell-value! (hash-ref env var) value))


; 通过多个绑定扩展环境:
(define (env-extend* env vars values)
  (match `(,vars ,values)
    [`((,v . ,vars) (,val . ,values))
     ; =>
     (env-extend* (hash-set env v (make-cell val)) vars values)]
    
    [`(() ())
     ; =>
     env]))


; 通过多次赋值改变环境:
(define (env-set!* env vars values)
  (match `(,vars ,values)
    [`((,v . ,vars) (,val . ,values))
     ; =>
     (begin
       (env-set! env v val)
       (env-set!* env vars values))]
    
    [`(() ())
     ; =>
     (void)]))


;; 计算测试。


; 定义新的语法,使测试看起来更漂亮:
(define-syntax 
  test-eval 
  (syntax-rules (====)
    [(_ program ==== value)
     (let ((result (eval (quote program) (env-initial))))
       (when (not (equal? program value))
         (error "test failed!")))]))


(test-eval
  ((lambda (x) (+ 3 4)) 20)
  ====
  7)


(test-eval
  (letrec ((f (lambda (n) 
                 (if (<= n 1)
                     1
                     (* n (f (- n 1)))))))
    (f 5))
  ====
  120)


(test-eval
  (let ((x 100))
    (begin
      (set! x 20)
      x))
  ====
  20)


(test-eval
  (let ((x 1000))
    (begin (let ((x 10))
             20)
           x))
  ====
  1000)


;; 程序被翻译成一个letrec表达式。


(define (define->binding define)
  (match define
    [`(define (,f . ,formals) ,body)
     ; =>
     `(,f (lambda ,formals ,body))]
    
    [`(define ,v ,value)
     ; =>
     `(,v ,value)]
    
    [else 
     ; =>
     `(,(gensym) ,define)]))


(define (transform-top-level defines)
  `(letrec ,(map define->binding defines)
     (void)))


(define (eval-program program)
  (eval (transform-top-level program) (env-initial)))


(define (read-all)
  (let ((next (read)))
    (if (eof-object? next)
        '()
        (cons next (read-all)))))


; 读入一个程序并计算:
(eval-program (read-all))

复制代码


下载源代码,请点击
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

相关推荐

七夕前学起来,程序员的浪漫:三十行代码实现用她的名字作幅画

hello,各位小伙伴们大家早上|中文|晚上|凌晨好,相信看这篇文章的有很多新朋友,估计也有少量的老朋友,首先做个简短的自我介绍,我是一灰灰,码农界的资深搬运工;今天呢,没有站在我身边的捧哏老师,那就...

127.手摇计算机的收藏(我的民间收藏笔记)

1970年代前后,我国生产的手摇计算机,主要有上海飞鱼牌和通用牌手摇计算机,天津文化牌手摇计算机。这几种手摇计算机的收藏价,目前很不统一。品相好又能使用的收藏价大概为1500—7000元。品相不好又...

计算机毕业设计Hadoop+Hive+PySpark小说推荐系统 小说可视化

基于Spark+hadoop大数据小说数据分析推荐系统(完整系统源码+数据库+开发笔记+详细部署教程+虚拟机分布式启动教程)直拍源码包部署爬虫可用基于用户协同过滤算法开发技术介绍编辑器:Pychar...

win7系统exe病毒文件夹怎么删除

Win7系统中exe病毒文件夹如何删除?下面为大家提供解决办法,快来了解吧!1、按下Win+R快捷键,输入gpedit.msc,所示,即可打开组策略编辑器。2、依次展开计算机配置下的管理模板,进入...

Windows 10 网络搜索设计太反人类?教你如何彻底关闭它

来源:太平洋电脑网我们知道微软在Windows10中,特别加强了系统的搜索功能,但Windows10的搜索的确很难称得上好用。抛开效率低下、呈现结果少、造成系统卡顿等老生常谈的问题不论,在功能设计...

win7系统exe病毒文件夹怎么删除?

经常遇到病毒文件夹,它们通常是带有exe后缀的文件夹名称,双击后会复制病毒。今天就教大家如何删除这些病毒文件夹。1、打开开始菜单,点击运行按钮;或者按下Win+R键,即可开启运行对话框。2、运行窗...

通过代码编写电脑关机程序

大家好,我是Anyday这期给大家分享的电脑小知识是通过代码编写程序进行关机。首先在桌面右键新建一个文本文档双击打开新建文档,在里面输入shutdown–s–t0,这就是我们上一期的关机代码(聪...

可视化程序设计必备书:从零开始Qt可视化程序设计

“可视化程序设计”是理工科极为重要的一门专业课程,实践性很强。其教学目标是使学生掌握可视化程序设计的基本方法、编程技能并具备上机调试能力,熟悉界面设计,掌握各种常用类(有些开发工具称控件,实...

重要通知!25年公务员专业参考目录已出!

大家关心的2025年江苏省公务员考试消息有了!一年一度江苏省公开征求对《江苏省2025年度考试录用公务员专业参考目录》的意见和建议公告出了!各地的公务员专业参考目录其实都查不多,江苏针对今年的具体情况...

计算机二级考试中的一些注意事项

科教武汉【计算机二级考试中的一些注意事项】1、要合理安排做题时间可以先通过观察整个题目的题形,判断整个试卷的难点,通过观察题型然后确定自己的应对策。选择题建议用时15-20分钟为好。自己要有一个时间...

天津专升本计算机知识点 选定文件和文件夹

在Windows7系统中,进行选定,包括多种,考试重点内容有三种。①选定多个连续的文件或文件夹,可用Shift键配合鼠标进行选定②选定多个不连续的文件或文件夹,可用Ctrl键配合鼠标进行选定③撤销某...

最新发布!四川这些岗位急需紧缺人才

12月17日,《四川省人力资源服务业急需紧缺人才目录》发布。据介绍,《四川省人力资源服务业急需紧缺人才目录》采集600余家用人单位信息,调查整理了40余家用人单位需求,从收集的上千条岗位信息中分析出3...

最新!普通高等学校本科专业目录(2024年)!共816种本科专业

高考成绩已定,目前最重要的,就是填报高考志愿了!!!(点击查看:广西2024高考分数线、一分一档表公布!今天开始填志愿!附前3年高考分数线、一分一档表)除了要在1308所本科大学中选出自己(孩子)喜欢...

cad文件夹加密

我学计算机辅助设计,常用CAD绘制图纸并存入文件夹。有时担心关机后设计被窃,便在网上寻找解决办法,最终找到了一种加密CAD文件夹的实用方法,有效保护了我的设计成果。1、首先,我们需要安装一款保护文件...

文件夹加密大师使用方法:快速加密文件指南

不想让他人看到私密文件?以下几种隐藏文件的方法各有优缺点,快来看看哪种最适合你!1、隐藏的文件夹2、首先,右击文件夹选择属性,在常规选项卡勾选隐藏,然后点击确定。3、若文件夹为隐藏状态,打开我的...