别写 js 编译器啦!用宏代替吧。

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

一个快速的 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 对本文的修订)

相关文章
相关标签/搜索