別寫 js 編譯器啦!用宏代替吧。

from http://jlongster.com/Stop-Writing-JavaScript-Compilers--Make-Macros-Instead

過去的一些年對 js 是不錯的。曾經備受 political 停滯折磨的屌絲語言,現在有了難以置信的發展平臺,活躍的大社區,還有一個進行迅速的標準化工作在進行。主要原因都是因爲互聯網,當然 node.js 也在此找到了自己的角色定位。

ES6 或者 Harmony http://wiki.ecmascript.org/doku.php?id=harmony:proposals ,是下一批 js 的進化。一切都終結了,所有的有趣的部分大都同意規範中的決定。他不僅是一個新標準;Chrome 和 Firefox 已經實現了很多 ES6 比如 generators , let declarations, 等。這是真的,而且 ES6 的鋪設之路只會更快,在未來小小的改進着 js。

關於 ES6 還有更多激動人心的事兒。但是我更激動的事兒不是它,而是低調的 sweet.js 小庫。

Sweet.js 給 js 帶來了宏。來跟我一起。宏常被濫用到嚇人。它真的是個好東西嗎?

是的,它是,我希望此文能解釋清楚。

宏是客觀正確的

有許多不同的 “宏” 概念,所以先不談這個。當我們說宏的時候我指的是可以定義一個小東西,它能被語法分析,並且轉成代碼。

C 語言把奇怪的 #define foo 5 叫做宏,但它們真的不是我們想要的宏。他是一種退化,本質上就是打開一個文件,搜索替換字符串,然後再保存成文件。它完全忽視了代碼結構,衝了一些不重要的事情上,他其實毫無意義。許多抄襲了這個功能的語言,聲稱有“宏”但是他們都是難以使用的閹割版。

真正的宏誕生於 1970 年的 Lisp ,用 defmacro (這基於了 10 年的研究成果,但是 Lisp 普及了這個概念)。這個驚人的想法體現在了 70s 80s 年代的論文甚至 Lisp 自身中。對 Lisp 來說這很自然,因爲它的代碼即數據。也就是說它能很容易的把代碼展開然後轉換其意思。

Lisp 證明了宏從根本上改變了此語言的生態,並且不出意料的,這一點其他語言很難擁有這種能力。

However,在其他有各種語法的語言(比如 js )搞類似的東西非常難。天真的做法是弄一個接受 AST 的功能,但是 ASTs 非常笨重,那樣你還不如寫一個編譯器呢。幸運的是,許多最近的研究解決啦這個問題,真正的 Lisp 風格的宏,被包含在了一些新的語言中,比如 julia http://docs.julialang.org/en/latest/manual/metaprogramming/ 和 rust http://static.rust-lang.org/doc/0.6/tutorial-macros.html 。

現在到了我們的 js https://github.com/mozilla/sweet.js。

一個快速的 Sweet.js 之旅

本文不是 js 宏的教程。只是想解釋,宏到底是怎樣從根本上增強 js 的進化。但是我想我需要先向從未見過宏的人們證明一下。

有複雜語法的語言用模式匹配來實現宏比較好。也就是說,你定義一個宏,有名字和一組模式。一旦名字被調用,編譯期代碼就被匹配和擴充啦。

macro define {
    rule { $x } => {
        var $x
    }

    rule { $x = $expr } => {
        var $x = $expr
    }
}

define y;
define y = 5;

上面的代碼展開爲:

var y;
var y = 5;

當運行 sweet.js 編譯器的時候。

當編譯器遇到 define ,他調用宏並且把每個 rule 規則,在後面的代碼上運行。當一個模式匹配成功,它返回 rule 中的規則。你可以在模式匹配中綁定標識符和 & 表達式,並在代碼中使用他們(用前綴 $),然後 sweet.js 將用原始模式上匹配的東西替換他們。

我們可以在 rule 中寫很多代碼來實現更高級的宏。無論如何,你開始遇到一個問題,當這樣用的時候:如果你在展開的代碼中聲明一個新變量,他很容易和已經存在的衝突,例如:

macro swap {
    rule { ($x, $y) } => {
        var tmp = $x;
        $x = $y;
        $y = tmp;
    }
}

var foo = 5;
var tmp = 6;
swap(foo, tmp);

swap 看起來像函數調用,但是注意宏是如何匹配括號和2個參數的。他可能擴展爲:

var foo = 5;
var tmp = 6;
var tmp = foo;
foo = tmp;
tmp = tmp;

宏創建的 tmp 和本地變量 tmp 衝突了。這是一個嚴重的問題,但是宏用衛生 http://en.wikipedia.org/wiki/Hygienic_macro 解決了這個問題。在擴展宏的過程中,它們追蹤作用域中的變量,重命名他們並維持正確的作用域。Sweet.js 完整實現了衛生,因此他不會形成上面的代碼,他會生成這樣的:

var foo = 5;
var tmp$1 = 6;
var tmp$2 = foo;
foo = tmp$1;
tmp$1 = tmp$2;

它看起來有點醜,但是注意 tmp 和他的不同。這讓創建複雜的宏帶來了強大的能力。

可是你想破壞衛生規則呢?或者你想處理某些格式的代碼,非常難模式匹配的那種?這不常見,但是你可以用 case 宏來做到。用這些宏,事實上 js 代碼在展開階段運行的,這時候你可以對它做任何事情(突然好邪惡)。

macro rand {
    case { _ $x } => {
        var r = Math.random();
        letstx $r = [makeValue(r)];
        return #{ var $x = $r }
    }
}

rand x;

上面會展開成:

var x$246 = 0.8367501533161177;

當然,它每次展開的隨機數字都不同。用 case 宏,case 代替 rule , case 後的代碼在擴展期執行,用 #{} 可以創建 “模板”,實現像 rule 在其他宏一樣的效果。現在將深入一些了,但是我將發佈一些教程,so 看我博客 http://feeds.feedburner.com/jlongster 如果你想知道如何寫這些。

這些例子雖然不是很重要,但是希望能展示出你可以輕鬆掛入編譯階段,並做一些高能行爲。

宏是模塊化的,編輯器不是

我喜歡 js 社區的一個事兒是大家不懼怕編譯器。有許多解析,檢查和改變 js 的庫,而且大家沒有畏懼的心理。

只可惜他們沒有真的擴展 js

原因是:他分離了社區。如果項目 A 實現了一個 js 語言擴展,項目 B 實現了另一個,我必須選擇一個啦。如果我用 A 的編譯器解析 B 的代碼,它將報錯。

另外,每個項目會有一個完全不同的編譯過程,每次都得學新的,我想要嘗試新的擴展是很恐怖的。(結果造成更少的人來嘗試我們的酷項目,然後酷項目就更少了,真是個悲傷的故事)。我用 Grunt,我經常需要花點時間爲一個不存在的項目寫 grunt task。

可能你是不喜歡編譯步驟的一些人。我理解,但是我鼓勵你跨越這道恐懼。像 Grunt http://gruntjs.com/ 一樣的工具讓這事兒自動在改變的時候構建,如果這麼做了你會獲益良多。

例如 traceur http://code.google.com/p/traceur-compiler/ 是一個非常酷的項目,把許多 ES6 特色轉到 es5。可它只有限制版本的 generators 支持。我們想說,我要用 regenerator https://github.com/facebook/regenerator 來代替,因爲它在編譯 yield 表達式的時候更酷。

我不能可靠的完成這個,因爲 traceur 可能實現 es5 特性的編譯器的時候不知道有這個 regenerator.

現在我們很幸運,因爲標準的編譯器比如 esprima http://esprima.org/ 支持了這個新的 es6 特性語法,因此很多項目將要認識到它了。但是把代碼流傳在不同的多個編譯器之間不是個好主意。不僅僅慢,而且不可靠,並且這個工具鏈難以置信的不好弄懂。

這流程就像這樣
圖片描述
我不認爲任何人真的這麼幹,因爲它不是可組合的。最後結果,我們不得不在一羣巨大的編譯器之間做選擇。

用宏,流程看起來是這樣:
圖片描述
只有一個編譯步驟,而且我們告訴 sweet.js 哪個模塊要用什麼順序加載。 sweet.js 註冊要加載的模塊並用他們擴展你的代碼

你可以爲你的項目設置一個理想的工作流。我的步驟:配置 grunt 運行 sweet.js 在所有的後端和前端 js (看我的 gruntfile https://gist.github.com/jlongster/8045898)。我運行 grunt watch 我想開發的時候,一旦有代碼改動,文件就自動的編譯,並帶上了 sourcemaps。如果我看到一個別人寫的宏,我只是 npm install 這命令告訴 sweet.js 加載它到我的 gruntfile 中,然後它就可用啦。注意所有的宏,sourcemaps 都生成好了,所以 debugging 也是很自然的。

這可能讓 js 從落後的代碼基礎和緩慢的標準化的束縛中解放出來。如果你可以配置語言的特性碎片,你將給社區很多能力來作爲討論的一部分,因爲他們能更早實現這個特性。

es6 是個偉大的起點,像非結構化賦值和類是純語法的增強,但是距離廣泛應用還很遠。我在弄一個 es6-macro https://github.com/jlongster/es6-macros 項目,用宏實現 es6 的很多特色。你能選取想要的並且現在就開始用 es6 啦。其他的還有像 Nate Faubion https://github.com/natefaubion/ 的卓越的 pattern matching libary https://github.com/natefaubion/sparkler。

sweet.js 現在還不支持 es6 模塊,但是你可以給編譯器加載一組宏,未來會在文件中加入 es6 模塊語法來加載特殊的模塊

一個好的 Clojure 例子,core.async https://github.com/clojure/core.async 庫提供了一點兒操作符其實是宏。當 go 塊語法出現,一個宏被調用了,完全的轉換代碼爲一個狀態機。它們可以實現類似的事情來轉成生成器 generators,那讓你暫停和繼續支持代碼,作爲一個庫只因有宏(原生核心語言根本不知道發生了啥)。

當然,不是所有的東西都能成爲宏。 ECMA 標準化流程將一直是需要的,有些事兒還是需要原生的支持來實現複雜的功能。但是我的噴點是很多人們想要的 js 的改進能輕鬆用宏來實現。

這就是爲什麼我對 sweet.js http://sweetjs.org/ 很激動。請記住它依然處於很早期,但是它的開發很活躍。我將教大家如何寫宏在以後的博文中。感興趣的話請關注我的博客 http://feeds.feedburner.com/jlongster

(感謝 Tim Disney 和 Nate Faubion 對本文的修訂)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章