實現一門程式語言對任何程式設計師來說都是值得擁有的經驗,因為它能加深你對計算原理的理解,並且還很有趣。
在這篇文章中,我已經讓整個過程迴歸到它的本質:為一種函式式(圖靈等價)程式語言設計7行程式碼的直譯器。大概只需要3分鐘就能實現
這個7行程式碼的直譯器展示了在眾多直譯器中同時存在的一個可升級的體系結構–eval/apply設計模式。《Structure and Interpretation of Computer Programs》這本書提到過該模式。
在這篇文章中總計有三門語言的實現:
- 一個是scheme語言的7行,3分鐘實現的直譯器
- 一個是Racket語言的重實現
- 最後一個是100行、“1-afternoon”直譯器,它實現了高階繫結形式、顯示遞迴、額外作用、高階函式式等等
對於掌握一門更豐富的語言來說,最後一個直譯器是一個好起點
一個小型(圖靈機等價)語言
最容易實現的一門程式語言是一個叫做λ運算的極簡單、高階函數語言程式設計語言
λ運算實際上存在於所有主要的功能性語言的核心中:Haskell, Scheme、 ML,但是它也存在於JavaScript、Python、Ruby中。它甚至隱藏在Java中,如果你知道到哪裡去找它。
歷史簡介
1929年Alonzo Church開發出λ演算
在那時,lambda calculus不被叫做程式語言因為沒有計算機,所以沒有程式設計的概念。
它僅僅是一個推演函式的數學標記。
幸運的是,Alonzo Church有一個叫作艾倫·圖靈的哲學博士。
艾倫·圖靈定義了圖靈機,圖靈機成了第一個被接受的通用計算機定義
不久後發現lambda calculus和圖靈機是等價的:任何用λ演算描述的功能可以在圖靈機上實現;並且在圖靈機上實現的任何功能可以用λ演算描述
值得注意的是在lambda calculus中僅有三種表示式:變數引用,匿名函式、函式呼叫
匿名函式:
1 |
(λv.e) |
匿名函式以”λ-.”標記開始,所以 (λ v . e)函式用來接收一個引數v並返回值e。
1 |
如果你用JavaScript程式設計,格式function (v) { return e ; }是相同的。 |
函式呼叫:
1 |
(fe) |
函式呼叫用兩個臨近的表示式表示:(f e)
1 2 3 |
f(e) 在JavaScript中(或者其他任何語言),寫為f(e) |
Examples
1 2 3 4 |
(λ x . x) 例如: 恆等函式(identity function),僅僅返回它的引數值,簡單地寫為(λ x . x) |
1 |
((λ x . x) (λ a . a)) |
我們可以將這個恆等函式應用到一個恆等函式上:
((λ x . x) (λ a . a))(僅返回這個恆等函式本身)
1 |
(((λ f . (λ x . (f x))) (λ a . a)) (λ b . b)) |
這兒有一個更有趣的程式:
1 |
(((λ f . (λ x . (f x))) (λ a . a)) (λ b . b)) |
你能弄清楚它是幹嘛的?
等一下!見鬼,這怎麼算一門程式語言?
乍一看,這門簡單語言好像缺乏遞迴和迭代,更不用說數字、布林值、條件語句、資料結構和剩餘其他的。這樣的語言怎麼可能成為通用的呢?
λ演算實現圖靈機-等價的方式是通過兩種最酷的方式:
邱奇編碼(Church encoding)和Y combinator(美國著名企業孵化器)
1 |
((λ f . (f f)) (λ f . (f f))) |
我已經寫了兩篇關於Y combinator和邱奇編碼的文章。
但是,你如果不想讀它們的話,我可以明確的告訴你比起你期望的僅一個((λ f . (f f)) (λ f . (f f)))程式來說 有更多的 lambda calculus知識。
表面上開始的程式叫做Ω,如果你嘗試執行它的話,它不會終止(想一下你是否明白其中原因)
實現λ演算
下面是基於Scheme語言標準(R5RS)的7行、3分鐘λ演算直譯器。在術語中,它是一個依賴環境的指示直譯器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
; eval takes an expression and an environment to a value (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 takes a function and an argument to a value (define (apply f x) (eval (cddr (car f)) (cons (list (cadr (car f)) x) (cdr f)))) ; read and parse stdin, then evaluate: (display (eval (read) '())) (newline) This code will read a program from stdin, parse it, evaluate it and print the result. (It's 7 lines without the comments and blank lines.) |
程式碼將從檔案中讀入程式、分析、求值最後列印值(這是一段沒有註釋和空白行的7行程式碼)
Schema語言的read函式使得詞法分析和語法分析簡單化。只要你想處於語法“平衡圓括號”(符號式)世界裡。
(如果不想的話,你必須鑽研語法分析,你可以從我寫的一篇語法分析文章開始)
在Scheme語言中,read函式從檔案獲取加括號的輸入並把它分析然後生成樹
函式eval 和 apply構成了直譯器的核心。即使我們使用的是Scheme語言,我們仍給出了函式概念上的“簽名”
1 2 3 4 5 6 |
eval : Expression * Environment -> Value apply : Value * Value -> Value Environment = Variable -> Value Value = Closure Closure = Lambda * Environment |
eval函式將一個表示式和環境變數賦給一個值。表示式可以是一個變數、λ術語或者是一個應用。
一個環境值是從變數到值的對映,用來定義一個開項的自由變數(開項用來存放出現的沒有繫結的變數)。想一下這個例子,表示式(λ x . z)是開項,因為我們不知道z是什麼。
因為我們使用Scheme語言標準(R5RS),所以用聯合列表來定義環境值
閉項是一個函式的編碼,這個函式使用定義自由變數的環境值來匹配lambda 表示式來。換句話說來說,閉項關閉了一個開項
Racket中有一種更簡潔的實現
Racket是Scheme的一種方言,功能齊備強大。它提供了一個整頓直譯器的匹配構造機制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#lang racket ; bring in the match library: (require racket/match) ; eval matches on the type of expression: (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 destructures the function with a match too: (define (apply f x) (match f [`(closure (λ ,v . ,body) ,env) (eval body (cons `(,v ,x) env))])) ; read in, parse and evaluate: (display (eval (read) '())) (newline) |
這一種更加龐大,但是理解起來也更容易、更簡單
一門更加龐大的語言
λ演算是一門極小的語言。儘管如此,直譯器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).
現在在語言中新增三種高階形式:
- 函式定義:(define (proc-name var …) expr).
- 全域性定義:(define var expr)
- 高階表示式:expr
下面是完整的直譯器,包含測試程式碼和測試用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
#lang racket (require racket/match) ;; Evaluation toggles between eval and apply. ; eval dispatches on the type of expression: (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))])) ; a handy wrapper for 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*))) ; applies a procedure to arguments: (define (apply-proc f values) (match f [`(closure (lambda ,vs ,body) ,env) ; => (eval body (env-extend* env vs values))] [`(primitive ,p) ; => (apply p values)])) ;; Environments map variables to mutable cells ;; containing values. (define-struct cell ([value #:mutable])) ; empty environment: (define (env-empty) (hash)) ; initial environment, with bindings for primitives: (define (env-initial) (env-extend* (env-empty) '(+ - / * <= void display newline) (map (lambda (s) (list 'primitive s)) `(,+ ,- ,/ ,* ,<= ,void ,display ,newline)))) ; looks up a value: (define (env-lookup env var) (cell-value (hash-ref env var))) ; sets a value in an environment: (define (env-set! env var value) (set-cell-value! (hash-ref env var) value)) ; extends an environment with several bindings: (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])) ; mutates an environment with several assignments: (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)])) ;; Evaluation tests. ; define new syntax to make tests look prettier: (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) ;; Programs are translated into a single letrec expression. (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))))) ; read in a program, and evaluate: (eval-program (read-all)) |
你可以從這裡下載原始碼:minilang.rkt.
在這裡
你應該儘可能快的通過修改最新的直譯器為程式語言徹底檢驗新的想法
如果你想使用含有不同語法的語言,你可以建立一個解析器,將符號式轉存出來。
使用這種方法,可以容易把句法設計與語義設計分離出來
更多資源:
- 最近,MIT通過讓學生做一個直譯器來入門電腦科學。那門課的教科書, 《Structure and Interpretation of Computer Programs》是現代的經典。
- 《Lisp in Small Pieces》是一本出色介紹函式性程式語言的著作。它可能會成為我即將到來的指令碼語言設計和實現的教科書。
- 關於 the Y combinator 、 Church encodings的文章
- 關於implementing first-class macros的文章。直譯器使用了同樣的 eval/apply架構。