2

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 对本文的修订)


tcdona
461 声望32 粉丝

may the money keep with you —— monkey


引用和评论

0 条评论