7行程式碼,3分鐘:從無到有實現一門程式語言

Calarence發表於2015-03-13

實現一門程式語言對任何程式設計師來說都是值得擁有的經驗,因為它能加深你對計算原理的理解,並且還很有趣。

在這篇文章中,我已經讓整個過程迴歸到它的本質:為一種函式式(圖靈等價)程式語言設計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中僅有三種表示式:變數引用,匿名函式、函式呼叫

匿名函式:

匿名函式以”λ-.”標記開始,所以 (λ v . e)函式用來接收一個引數v並返回值e。

函式呼叫:

函式呼叫用兩個臨近的表示式表示:(f e)

Examples

我們可以將這個恆等函式應用到一個恆等函式上:

((λ x . x) (λ a . a))(僅返回這個恆等函式本身)

這兒有一個更有趣的程式:

你能弄清楚它是幹嘛的?

等一下!見鬼,這怎麼算一門程式語言?

乍一看,這門簡單語言好像缺乏遞迴和迭代,更不用說數字、布林值、條件語句、資料結構和剩餘其他的。這樣的語言怎麼可能成為通用的呢?

λ演算實現圖靈機-等價的方式是通過兩種最酷的方式:

邱奇編碼(Church encoding)和Y combinator(美國著名企業孵化器)

我已經寫了兩篇關於Y combinator和邱奇編碼的文章。

但是,你如果不想讀它們的話,我可以明確的告訴你比起你期望的僅一個((λ f . (f f)) (λ f . (f f)))程式來說 有更多的 lambda calculus知識。

表面上開始的程式叫做Ω,如果你嘗試執行它的話,它不會終止(想一下你是否明白其中原因)

實現λ演算

下面是基於Scheme語言標準(R5RS)的7行、3分鐘λ演算直譯器。在術語中,它是一個依賴環境的指示直譯器

程式碼將從檔案中讀入程式、分析、求值最後列印值(這是一段沒有註釋和空白行的7行程式碼)

Schema語言的read函式使得詞法分析和語法分析簡單化。只要你想處於語法“平衡圓括號”(符號式)世界裡。

(如果不想的話,你必須鑽研語法分析,你可以從我寫的一篇語法分析文章開始)
在Scheme語言中,read函式從檔案獲取加括號的輸入並把它分析然後生成樹
函式eval 和 apply構成了直譯器的核心。即使我們使用的是Scheme語言,我們仍給出了函式概念上的“簽名”

eval函式將一個表示式和環境變數賦給一個值。表示式可以是一個變數、λ術語或者是一個應用。

一個環境值是從變數到值的對映,用來定義一個開項的自由變數(開項用來存放出現的沒有繫結的變數)。想一下這個例子,表示式(λ x . z)是開項,因為我們不知道z是什麼。

因為我們使用Scheme語言標準(R5RS),所以用聯合列表來定義環境值

閉項是一個函式的編碼,這個函式使用定義自由變數的環境值來匹配lambda 表示式來。換句話說來說,閉項關閉了一個開項

Racket中有一種更簡潔的實現

Racket是Scheme的一種方言,功能齊備強大。它提供了一個整頓直譯器的匹配構造機制。

這一種更加龐大,但是理解起來也更容易、更簡單

一門更加龐大的語言

λ演算是一門極小的語言。儘管如此,直譯器eval/apply的設計可以升級到更加龐大的語言。

例如,用大約100行的程式碼,我們可以為Scheme本身相當大的一個子集實現直譯器

考慮一門含有不同表示式分類的語言:

  1. 變數引用:除x,foo,save_file
  2. 數值和布林型別的常量:除300,3.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).

現在在語言中新增三種高階形式:

  1. 函式定義:(define (proc-name var …) expr).
  2. 全域性定義:(define var expr)
  3. 高階表示式:expr

下面是完整的直譯器,包含測試程式碼和測試用例:

你可以從這裡下載原始碼:minilang.rkt.

在這裡

你應該儘可能快的通過修改最新的直譯器為程式語言徹底檢驗新的想法

如果你想使用含有不同語法的語言,你可以建立一個解析器,將符號式轉存出來。

使用這種方法,可以容易把句法設計與語義設計分離出來

更多資源:

相關文章