from http://jlongster.com/Stop-Writing-JavaScript-Compilers--Make-Macros-Insteadphp
過去的一些年對 js 是不錯的。曾經備受 political 停滯折磨的屌絲語言,如今有了難以置信的發展平臺,活躍的大社區,還有一個進行迅速的標準化工做在進行。主要緣由都是由於互聯網,固然 node.js 也在此找到了本身的角色定位。html
ES6 或者 Harmony http://wiki.ecmascript.org/doku.php?id=harmony:proposals ,是下一批 js 的進化。一切都終結了,全部的有趣的部分大都贊成規範中的決定。他不只是一個新標準;Chrome 和 Firefox 已經實現了不少 ES6 好比 generators , let declarations, 等。這是真的,並且 ES6 的鋪設之路只會更快,在將來小小的改進着 js。前端
關於 ES6 還有更多激動人心的事兒。可是我更激動的事兒不是它,而是低調的 sweet.js 小庫。node
Sweet.js 給 js 帶來了宏。來跟我一塊兒。宏常被濫用到嚇人。它真的是個好東西嗎?git
是的,它是,我但願此文能解釋清楚。es6
有許多不一樣的 「宏」 概念,因此先不談這個。當咱們說宏的時候我指的是能夠定義一個小東西,它能被語法分析,而且轉成代碼。github
C 語言把奇怪的 #define foo 5 叫作宏,但它們真的不是咱們想要的宏。他是一種退化,本質上就是打開一個文件,搜索替換字符串,而後再保存成文件。它徹底忽視了代碼結構,衝了一些不重要的事情上,他其實毫無心義。許多抄襲了這個功能的語言,聲稱有「宏」可是他們都是難以使用的閹割版。npm
真正的宏誕生於 1970 年的 Lisp ,用 defmacro (這基於了 10 年的研究成果,可是 Lisp 普及了這個概念)。這個驚人的想法體如今了 70s 80s 年代的論文甚至 Lisp 自身中。對 Lisp 來講這很天然,由於它的代碼即數據。也就是說它能很容易的把代碼展開而後轉換其意思。後端
Lisp 證實了宏從根本上改變了此語言的生態,而且不出意料的,這一點其餘語言很難擁有這種能力。dom
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。
本文不是 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 對本文的修訂)