将语言编译为 Lambda Calculus(译)

来源:互联网 发布:js div position 编辑:程序博客网 时间:2024/05/17 22:33

(注:原文发表于 http://matt.might.net/articles/compiling-up-to-lambda-calculus/)

我教授的编译器课程总是由一节关于 λ 演算的完整介绍开始。

It leaves behind only the dedicated.

Y Combinator

Barendregt is a helluva drug.

λ 演算是一门微型编程语言。

尽管它只包含了函数应用、变量引用以及匿名函数的形式,它仍旧等价于一个图灵机。

然而,等价性并不是显而易见的。

等价性的一个方向是构造出一个有效方法来计算 λ 演算中一个表达式的值。

(我们知道如何以图灵机实现那样有效的方法。)

另一方向则需要更多的考虑:如何将一门简单的语言变换为更现代化的编程语言,例如提供我们所期望的数字、布尔值、条件控制、列表以及递归?

(一旦你拥有了那些功能就可以很容易的模拟图灵机了。)

为了说明这些是如何完成的,这篇文章的内容包含了一个 Racket 编写的编译器。该编译器将一个精巧的、清晰而通用的函数式编程语言编译为纯的 λ 演算。

当构建一个编译器时,这些技术是非常有用的:一旦匿名函数可用,你可以使用语法变换(原文为「desugaring」)实现未完成的功能。

继续阅读下面的阐释、例子与代码。

λ 演算

λ 演算是一门只有三个表达式的语言:

  • 变量引用,例如 vfoo
  • 函数应用,例如 (f x)(f (g x));以及
  • 匿名函数,例如 (λ (v) (+ v 1))

或者,用 BNF 来表达的话:

<exp> ::= <var>       |  (<exp> <exp>)       |  (λ (<var>) <exp>)

不过,别被这『小巧玲珑的身段』给骗了: λ 演算是图灵完备的。

λ 演算是数学的汇编语言。

麻雀虽小,五脏俱全

这门我们要将其编译为 λ 演算的语言是一个小型的函数式语言,一个清晰而图灵完备的 Scheme 语言的子集:

<exp> ::= <var>        |  #t        |  #f        |  (if  <exp> <exp> <exp>)        |  (and <exp> <exp>)        |  (or  <exp> <exp>)        |  <nat>        |  (zero? <exp>)        |  (- <exp> <exp>)        |  (= <exp> <exp>)        |  (+ <exp> <exp>)        |  (* <exp> <exp>)        |  <lam>        |  (let ((<var> <exp>) ...) <exp>)        |  (letrec ((<var> <lam>)) <exp>)        |  (cons <exp> <exp>)        |  (car  <exp>)        |  (cdr  <exp>)        |  (pair? <exp>)        |  (null? <exp>)        |  '()        |  (<exp> <exp> ...)<lam> ::= (λ (<var> ...) <exp>)

我们将使用丘奇编码来为我们的小型语言提供原子与复合数据结构。

丘奇编码是一种将数字、布尔值或一个列表表示成程序的方法。

Compile 函数

通过对表达式的匹配与分发,compile 函数『驱动』着转移过程;以下为每种匹配情况的示例代码:

; Compilation:(define (compile exp)  (match exp    ; Symbols stay the same:    [(? symbol?)         exp]    ; Boolean and conditionals:    [#t                  ...]    [#f                  ...]    [`(if ,cond ,t ,f)   ...]    [`(and ,a ,b)        ...]    [`(or ,a ,b)         ...]    ; Numerals:    [(? integer?)        ...]    [`(zero? ,exp)       ...]    [`(- ,x ,y)          ...]    [`(+ ,x ,y)          ...]    [`(* ,x ,y)          ...]    [`(= ,x ,y)          ...]    ; Lists:    [ (quote '())        ...]    [`(cons  ,car ,cdr)  ...]    [`(car   ,list)      ...]    [`(cdr   ,list)      ...]    [`(pair? ,list)      ...]    [`(null? ,list)      ...]    ; Lambdas:    [`() ,exp)           ...]    [`(,v) ,exp)         ...]    [`(,v ,vs ...) ,exp) ...]    ; Binding forms:    [`(let ((,v ,exp) ...) ,body)  ...]    [`(letrec [(,f ,lam)] ,body)   ...]    ; Application -- must be last:    [`(,f)                    ...]    [`(,f ,exp)               ...]    [`(,f ,exp ,rest ...)     ...]    [else       (display (format "unknown exp: ~s~n" exp))     (error "unknown expression")]))

以上的框架突出了在编译器与解释器的构造中典型的编程模式。

多参函数

多个参数的函数会被规约为单参函数。与接受多个参数相反,一个程序接受第一个参数并返回一个接受剩余参数的程序。

具体来说,一个多参的 lambda 表达式:

(λ (v1 ... vN) body)

会被转译为:

(λ (v1)   (λ (v2)   ...    (λ (vN)     body)))

同时,一个多参的函数应用:

(f arg1 ... argN)

会变成:

(... ((f arg1) arg2) ... argN)

以下是在 compile 函数中用来变换 λ 表达式的语句:

; Lambdas:[`(λ () ,exp)            ; => `(λ (_)  ,(compile exp))] [`(λ (,v) ,exp)          ; => `(λ (,v) ,(compile exp))][`(λ (,v ,vs ...) ,exp) ; => `(λ (,v)    ,(compile `(λ (,@vs) ,exp)))]

而以下是位于 compile 函数末尾处用来处理函数应用的情形:

; Application -- must be last:[`(,f)  ; => (compile `(,(compile f) ,VOID))][`(,f ,exp) ; => `(,(compile f) ,(compile exp))][`(,f ,exp ,rest ...) ; => (compile `((,f ,exp) ,@rest))][else   ; => (display (format "unknown exp: ~s~n" exp)) (error "unknown expression")]))

这样的技术被称为柯里化。

布尔值与条件控制

丘奇编码最主要的手段便是将数据编码为一个个计算过程。

考虑布尔值 true 和 false。

true 和 false 是怎样被使用的呢?

他们会以条件语句出现在一个 if 形式之中。

一个布尔值将在两个潜在的计算过程分支中选择其一来进行。

所以,一个布尔值需要接受两个计算过程(已被编码的函数)并执行二者中的一个。

true 编码后会执行『true』分支的计算,而 false 则会执行相应的『false』部分:

; Booleans.(define TRUE  `(t) (f) (t ,VOID))))(define FALSE `(t) (f) (f ,VOID))))

相应的,compile 函数中会将条件语句转换成计算过程:

; Boolean and conditionals:[#t              TRUE][#f              FALSE][`(if ,cond ,t ,f) ; => (compile `(,cond (λ () ,t) (λ () ,f)))][`(and ,a ,b) ; => (compile `(if ,a ,b #f))][`(or ,a ,b) ; => (compile `(if ,a #t ,b))]

丘奇编码下的数字

有很多种将数字编码为计算过程的方法。

考虑到数字的用途的话:计数、测度、索引、排序以及迭代。

迭代被证明是编码数字的一般方法。

也就是说,我们可以将数字 n 编码为一个函数调用另一个函数 n 次。

程序 church-numeral 接受一个自然数,生成一个拥有以下签名的函数 ƒ

f:(αα)αα

于是:

f(g)(z)=gn(z)

church-numberal 的代码并不长:

; Church numerals.(define (church-numeral n)  (define (apply-n f n z)    (cond      [(= n 0)  z]      [else     `(,f ,(apply-n f (- n 1) z))]))  (cond    [(= n 0)    `(f) (z) z))]    [else       `(f) (z)                           ,(apply-n 'f n 'z)))]))

在这种迭代表示方法下,我们可以编码加法、减法、乘法以及数值相等:

(define ZERO? `(n)                 ((n (_) ,FALSE)) ,TRUE)))(define SUM '(n)               (m)                 (f)                   (z)                     ((m f) ((n f) z)))))))(define MUL '(n)               (m)                 (f)                   (z)                     ((m (n f)) z))))))(define PRED '(n)                (f)                  (z)                    (((n (g) (h)                                   (h (g f)))))                      (u) z))                     (u) u))))))(define SUB `(n)               (m)                 ((m ,PRED) n))))

于是,compile 中相应的部分将会是:

; Numerals:[(? integer?)     (church-numeral exp)][`(zero? ,exp)   `(,ZERO? ,(compile exp))][`(- ,x ,y)      `((,SUB ,(compile x)) ,(compile y))][`(+ ,x ,y)      `((,SUM ,(compile x)) ,(compile y))][`(* ,x ,y)      `((,MUL ,(compile x)) ,(compile y))][`(= ,x ,y)       (compile `(and (zero? (- ,x ,y))                 (zero? (- ,y ,x))))]

列表的表示

列表是一个只拥有单一操作的一种对象:解构中的匹配(即 cons,译者住)。

列表的匹配接受两个操作符:一个调用列表「头」(head)与剩余部分(rest)的函数,而另一函数则会在列表为空时被调用。

于是,一个丘奇编码的列表是一个函数,它接受两个操作符 —— 一个函数调用列表的「头」(head)与剩余部分(rest),另一个函数在列表为空时被调用:

; Lists.(define CONS `(car)                 (cdr)                  (on-cons)                    (on-nil)                      ((on-cons car) cdr))))))(define NIL `(on-cons)               (on-nil)                 (on-nil ,VOID))))(define CAR `(list)               ((list (car)                       (cdr)                         car)))                ,ERROR)))(define CDR `(list)               ((list (car)                       (cdr)                         cdr)))                ,ERROR)))(define PAIR? `(list)                 ((list (_) (_) ,TRUE)))                  (_) ,FALSE))))(define NULL? `(list)                 ((list (_) (_) ,FALSE)))                  (_) ,TRUE))))

所以在compile 函数中紧紧需要添加以下这些:

; Lists:[ (quote '())         NIL][`(cons  ,car ,cdr)  `((,CONS ,(compile car))                ,(compile cdr))][`(car   ,list)      `(,CAR   ,(compile list))][`(cdr   ,list)      `(,CDR   ,(compile list))][`(pair? ,list)      `(,PAIR? ,(compile list))][`(null? ,list)      `(,NULL? ,(compile list))]

let 的变换

一个 let 形式会变成一个 λ 表达式的直接应用:

具体来说,以下形式:

(let ((v1 exp1) ... (vN expN)) body)

会变成:

((λ (v1 ... vN) body) exp1 ... expN)

以下为在 compile 函数中处理 let 形式的代码:

; Binding forms:[`(let ((,v ,exp) ...) ,body) ; => (compile `((λ (,@v) ,body) ,@exp))]

递归:神奇的 Y Combinator

为了处理递归,我们将调用 Y combinator(我已经在另一篇文章中阐述了 Y combinator 和不动点理论)。

Y combinator 将一个递归函数视作一个非递归函数的不动点。

值得一提的是,在 λ 演算中 Y combinator 可以很直接地被表达出来:

; Recursion.(define Y '((y) (F) (F (x) (((y y) F) x)))))             (y) (F) (F (x) (((y y) F) x)))))))

这样一来,compile 便可以处理 letrec 了:

[`(letrec [(,f ,lam)] ,body) ; => (compile `(let ((,f (,Y (λ (,f) ,lam))))         ,body))]

外部函数接口(FFI):去丘奇化(Unchurchifiers)

事实上,编译到一个目标语言并不是特别有用,除非,语言提供了一种与目标语言交互的方法。

为了将编码后的数字、布尔值以及列表变回 Racket 的数据,我们需要「去丘奇化」:

; Unchurchification.(define (succ n) (+ n 1))(define (natify church-numeral)  ((church-numeral succ) 0))(define (boolify church-boolean)  ((church-boolean (_) #t)) (_) #f)))(define (listify f church-list)  ((church-list    (car) (cdr) (cons (f car) (listify f cdr)))))   (_) '())))

函数 natifyboolify 以及 listify 是相对应编译动作的逆操作。

实例:阶乘

考虑以下计算阶乘的程序 R1

(define R1 (compile `(letrec [(f (n)                                    (if (= n 0)                                       1                                       (* n (f (- n 1))))))]                       (f 5))))

其编译后的代码为:

((f) (f (f) (z) (f (f (f (f (f z)))))))))  (((y) (F) (F (x) (((y y) F) x)))))    (y) (F) (F (x) (((y y) F) x))))))   (f)     (n)       ((((((n)              ((n (_) (t) (f) (f (void) void))))))               (t) (f) (t (void) void))))))            (((n)                (m)                  ((m                    (n)                      (f)                        (z)                          (((n (g) (h) (h (g f)))))                            (u) z))                           (u) u))))))                   n)))              n)             (f) (z) z))))           (_)             ((n)                ((n (_) (t) (f) (f (void) void))))))                 (t) (f) (t (void) void))))))              (((n)                  (m)                    ((m                      (n)                        (f)                          (z)                            (((n (g) (h) (h (g f)))))                              (u) z))                             (u) u))))))                     n)))                (f) (z) z)))               n))))          (_) (t) (f) (f (void) void))))))         (_) (f) (z) (f z)))))        (_)          (((n) (m) (f) (z) ((m (n f)) z))))) n)           (f            (((n)                (m)                  ((m                    (n)                      (f)                        (z)                          (((n (g) (h) (h (g f)))))                            (u) z))                           (u) u))))))                   n)))              n)             (f) (z) (f z))))))))))))

接着,我们用 eval 对此求值并且「去丘奇化」,正如期望中的一样,我们的到了结果120:

{% highlight scheme %}

(natify (eval R1))
120
{% endhighlight %}

代码

Racket 源代码在此。

更多资源

  • 如果你对 λ 演算(以及『类型』)很感兴趣的话,Benjamin Pierce 的『橘书』是一本相关的标准教科书。
  • 我的关于用 Y combinator 在 Javascript 实现 「memoized」递归的文章。
  • 我的关于如何使用续延传递把 call/cc 和 异常转换至 λ 演算。
  • 我的关于编程语言的推荐书目。
0 0
原创粉丝点击