from http://jlongster.com/Writing-Your-First-Sweet.js-Macro
你将学会
- 写第一行宏
- 基础的模式匹配
- 如何用 sjs 编译器
- 使用 sourcemaps 来调试。
所有的教程和可用的编译 sweet.js 宏环境都躺在 repo https://github.com/jlongster/sweet.js-tutorials 中。下一个教程是递归的宏以及模式匹配 http://jlongster.com/Sweet.js-Tutorial--2--Recursive-Macros-and-Custom-Pattern-Classes
如果你完全不了解 sweet.js 和 js 宏,我推荐先阅读 http://jlongster.com/Stop-Writing-JavaScript-Compilers--Make-Macros-Instead。我尽早把这些表达出来,虽然要花些时间。你可以订阅 rss http://feeds.feedburner.com/jlongster 或者 follow 我的 twitter https://twitter.com/jlongster
来见见我们的宏编辑器
整个教程都围绕他进行,你可以交互式的左边写代码,右边会自动 expand 展开你的代码,并且展示结果 (ps:我们这里可能要上面看代码 (//注释分隔) 下面看结果了)
macro foo {
rule { $x } => { $x }
}
foo "Hi there!";
foo "Another string";
//
'Hi there!';
'Another string';
如果有报错 error 会有个大红条儿出现在底部,也可以单步调试。这在后面的教程中尤其有用哦~
下面有些链接,点击就会改变左边框中的代码,比如点击这里,看看变化,点击 revert 恢复原始代码。
macro bar {
rule { $x } => { $x }
}
bar "This text is different!";
bar "The macro name changed too";
//
'This text is different!';
'The macro name changed too';
当你想要写更大变化的代码,直接用在线编辑器 http://sweetjs.org/browser/editor.html 会更好啦
注意:本教程用的 sweet.js 提交在 ba9b6771678cb26af58dfa6b5b99d5b7eac75e2c 3月9号, sweet.js 仍然是早期阶段,所以你可能碰到 bugs.
让我们开始吧
宏用关键词 macro 创建。我们提供一个名字,以及一个模式匹配列表
macro foo {
rule { $x } => { $x + 'rule1' }
}
foo 5;
foo bar;
//
5 + 'rule1';
bar + 'rule1';
宏名字可以包含任何关键字,标识符或者标点。一个 punctuator 加标点者 是一个符号,比如:+ - * & ^ $ # 等等。只有 {} 这几个被当作分隔符的符号无效。
这里宏的唯一规则就是绑定 bind 第一个元素到 $x。它执行宏名字后面的模式匹配语法。当你给一个标识符加前缀 $ ,宏就会捕获匹配的元素,你可以在模板中输出匹配的值,否则,宏会照字面意思原样匹配口令就像 { 或 } 等任何你扔到代码里面的东西。
没有 $ ,宏会照字面意思原样匹配标识符。来看看,我们把上面宏里的 $x 改为 x
macro foo {
rule { x } => { $x + 'rule1' }
}
foo 5;
foo bar;
//
SyntaxError: [macro] Macro `foo` could not be matched with `5 ; foo bar ;...` 5: foo 5; ^
我们看到,这触发了一个错误。如果你想重置代码例子,只需要点击 “恢复 revert”,然后它又活过来啦。
我们坚持只用 x 他也会工作
macro foo {
rule { x } => { 'rule1' }
}
foo x;
//
'rule1';
但是如果我们调用任何非 x 的数据,都会触发宏的失败。它现在只是完全照字面意思原样的匹配 x 除非我们用 $x 来使得他成为一个,模式变量
来玩玩几个宏轻松下:
- ^ - 用标点当宏
- var - 简单到愚蠢的重构我们的 var
macro ^ {
rule { { $x } } => { wrapped($x) }
}
^{x};
foo(x, y, ^{z})
//
wrapped(x);
foo(x, y, wrapped(z));
let var = macro {
rule { [$x, $y] = $arr } => {
var $x = $arr[0];
var $y = $arr[1];
}
}
var [foo, bar] = arr;
//
var foo = arr[0];
var bar = arr[1];
多行模式
下面,我们来试试添加更多的规则和模式:
macro foo {
rule { => $x } => { $x + 'rule1' }
rule { [$x] } => { $x + 'rule2' }
rule { $x } => { $x + 'rule3' }
}
foo => 5;
foo 6;
foo [bar];
//
5 + 'rule1';
6 + 'rule3';
bar + 'rule2';
现在更好玩了。我们用不同的代码触发不同的匹配模式。第一个规则匹配 => 口令,然后把这个口令后面跟的任何东西一起绑定到 $x。 foo => 5 正确的匹配到 5 + 'rule1'。第二个规则剥去 [] 然后捕获到剩下的东西
=> 被视作一个单独的口令,因为es6 fat arrow functions http://wiki.ecmascript.org/doku.php?id=harmony:arrow_function_syntax 这么用啦。只要整个口令集都匹配,任何口令集都能正常工作,所以 =*> 也是可以滴~
macro foo {
rule { =*> $x } => { $x + 'rule1' }
rule { [$x] } => { $x + 'rule2' }
rule { $x } => { $x + 'rule3' }
}
foo =*> baller;
foo 6;
foo [bar];
//
baller + 'rule1';
6 + 'rule3';
bar + 'rule2';
规则的顺序是严格的。这是我们要学习的模式匹配的一个基本的原则。他是从顶至底匹配的,所以更细的模式应在包容性更强的模式上面。例如,[$x] 是比 $x 更细粒度的,如果你切换他们的顺序,foo [bar]将匹配到更包容的模式,也就是说有些就匹配不到了。比如:
macro foo {
rule { => $x } => { $x + 'rule1' }
rule { $x } => { $x + 'rule3' }
rule { [$x] } => { $x + 'rule2' }
}
foo => 5;
foo 6;
foo [bar];
//
5 + 'rule1';
6 + 'rule3';
[bar] + 'rule3';
模式类
当你用一个模式变量 $x,他匹配任何在其位置代表的东西,无论是字符串,数组表达式。。。。如果你要限定他匹配的类型咋办呢?
可以指定一个特殊的解析类 parse class。例如: $x:ident 只匹配标识符。有3个可用解析类:
- expr 表达式
- ident 一个标识符
- lit 一段文字
你可能会想,什么是 expr 呢?因为 expr 几乎可以是任何东西。默认情况下,sweet.js 是不贪婪的。他会找到最小匹配的语法。如果你尝试匹配 $x 为 bar(),$x 将仅仅被绑定到标识符 bar。如果你强制他为表达式,$x:expr 才能匹配到整个 bar() 表达式。
使用模式解析类,当出问题的时候,你会得到很好的匹配错误警告。
macro foo {
rule { $x:lit } => { $x + 'lit' }
rule { $x:ident } => { $x + 'ident' }
rule { $x:expr } => { $x + 'expr' }
}
foo 3;
foo "string";
foo bar;
foo [1, 2, 3];
foo baz();
//
3 + 'lit';
'string' + 'lit';
bar + 'ident';
[
1,
2,
3
] + 'expr';
baz + 'ident'();
3 和 "string" 匹配 lit,bar 匹配 ident。一个标识符是任意的 js 变量名,一个文本是任何的数字或者字符串常数。
数组以表达式来匹配,这没错。但是,baz()呢?前面提到,sweet.js是不贪婪的,他只匹配 baz。特别在这个情况下,因我们有了一个 $x:ident 其优先级高于 $x:expr,于是匹配了标识符baz。就算我们不用模式类,我们也有这个问题:
macro foo {
rule { $x } => { $x + 'any' }
rule { $x:expr } => { $x + 'expr' }
}
foo baz();
//
baz + 'any'();
如果我们真的要匹配整个表达式呢,我们需要用 $x:expr 并且要把他放到其他规则之上。
macro foo {
rule { $x:expr } => { $x + 'expr' }
rule { $x } => { $x + 'any' }
}
foo baz();
//
baz() + 'expr';
至少现在看不出来任何规则在他之下,因为 $x:expr 将匹配表达式和文本。
如果真的想匹配整个表达式,应一直在模式变量上用 expr 类
递归宏 和 let
宏展开是递归的,意思是在你的宏运行后 sweet.js 将再次展开代码。你会发现这个行为在更高级的宏(用数个可展开的步骤来转换代码)中非常好用。当你匹配一个规则,然后释放代码来再次运行宏来匹配另一个规则。在更进一步的教程中,你将看到它们。
macro foo {
rule { { $expr:expr } } => {
foo ($expr + 3)
}
rule { ($expr:expr) } => {
"expression: " + $expr
}
}
foo { 1 + 2 }
//
'expression: ' + 1 + 2 + 3;
一个很好用的功能是重写关键字。例如你可以实现一个 var 宏,重写内置的var,并加上 es6 非结构化 http://wiki.ecmascript.org/doku.php?id=harmony:destructuring。当然,如果非结构化没有被使用,你只需要简单的扩展到原生的 var 声明上。
这造成一个问题:你需要释放一个不递归展开宏的代码。这时候可能用 let 定义一个你自己的宏: let foo = macro { ... }。现在 foo 在你的展开代码中不会引用任何你的宏外面的 foo ,所以他不会被展开。如果你制作一个 var 宏,任何 var 你释放的都成为内置的 var。
来改变上面的例子到一个 let 宏:
let foo = macro {
rule { { $expr:expr } } => {
foo ($expr + 3)
}
rule { ($expr:expr) } => {
"expression: " + $expr
}
}
foo { 1 + 2 }
//
foo(1 + 2 + 3);
重复的模式
重复模式允许你一次捕获多重的模式实例。只要匹配就会这样。你可以用 ... 匹配重复的模式。
一个基础的例子: rule { ($name ...) } => { $name ... }。它匹配所有有括号的语法,然后在输出中剥去括号
一个重复模式匹配 0 到多次,所以他不能用来强制一个模式的存在,他会一直匹配,就算匹配的是空
macro basic {
rule { { $x (,) ... } } => {
wrapped($x (,) ...);
}
}
basic {} // expands to wrapped()
basic { x, y, z } // expands to wrapped(x, y, z)
为了让重复模式更有用, sweet.js 允许你定制一个分隔符,用来分开模式们。例如, rule { $name (,) ... } => { $name (,) ... } 说的是在匹配之间应有一个逗号,并且这个逗号同重复的 names 一同释放。你可以在调用代码中丢掉这个逗号(只是 $name ...) 那么他将只释放 names 的集合,忽略逗号。在 js 中用逗号分隔元素非常普遍 (想想 参数 args, 数组元素,等等)
最后,为了定制一个复杂的模式,你需要用到模式组。因为 ... 只是提前在一个简单的匹配变量上起作用,当你需要 "组" 一个模式,到一个简单的元素。你该这么做: rule { $($name = $init) (,) ... } => { $name ... }。注意这个额外的 $() 他创建了一个模式组。这个宏将匹配像 x=5, y=6, z=7 并释放 x y z 。你能可选的来用模式组和分隔符,当匹配和释放代码的时候,这允许你转换重复模式到任何其他想要的重复模式。
希望多玩玩 editor 能多感受一下。这里是一些给你瞎搞的宏~~:
- base 一个简单的重复模式
- function 一个简单的函数追踪
- var var 带着一些简单的结构化
- nested 混合的重复
macro basic {
rule { { $x (,) ... } } => {
wrapped($x (,) ...);
}
}
basic {}
basic { x, y, z }
//
wrapped();
wrapped(x, y, z);
let function = macro {
rule { $name ($args (,) ...) { $body ... } } => {
function $name($args (,) ...) {
console.log("called");
$body ...
}
}
}
function bar() {
var x = 2, y = 5;
console.log('hello');
}
//
function bar() {
console.log('called');
var x = 2, y = 5;
console.log('hello');
}
let var = macro {
rule { $([$name] = $expr:expr) (,) ... } => {
$(var $name = $expr[0]) ...
}
}
var [x] = arr, [y] = bar();
//
var x = arr[0];
var y = bar()[0];
macro foo {
rule { [$([$name ...] -> $init) (,) ...] } => {
$($(var $name = $init;) ...) ...
}
}
foo [[x y z] -> 3, [bar baz] -> 10]
//
var x = 3;
var y = 3;
var z = 3;
var bar = 10;
var baz = 10;
所有的这些例子都是故意简化的来方便简单的玩玩。var 宏,比如破坏了 var 因为你不再能正常的用内建的 var 啦。未来的教程中我们会深入更多的复杂的宏,将会解释这类事情。
重复模式是好用的,但是如果你想要更多的控制匹配的模式,一般用递归的宏来代替。它让你做一些了匹配 1 或者更多,捕获多个不同的模式,等等其他。同样的,更多的将会在以后的教程中!
==卫生==
这里我们不能谈卫生,即时他通常不影响你如何写宏。事实上,卫生的关键点是:它都是自动生效的,所以你不需要担心名字冲突。
所有在 sweet.js 中的宏都是卫生的,意思是标识符总是引用了正确的东西。如果一个宏创建了一个新的变量(比如用了 var ),他会只在宏的本身中有效。他不会和调用宏的代码中的另外的其他同名变量冲突。默认情况下,你不能引入一个新变量到一个无意识的作用域中。你能做一些工作来故意做到这一点,我们都把他放到未来的教程中
macro foo {
rule { $id = $init } => {
var $id = $init
}
rule { $init } => { var x = $init }
}
foo 5;
var x = 6;
foo y = 10;
var y = 11;
//
var x = 5;
var x$2 = 6;
var y = 10;
var y = 11;
上面的例子中,前2个例子创建了2个不同的变量,因为宏自己创建了一个 x ,和我们用 var 产生的是不同的。在后面的2个例子中,都引用了同样的 y ,因为我把他传给了宏
卫生改名是你看到 foo 改名到了 'foo$1234' 的原因。 sweet.js 确保每个变量都引用了正确的东西。他必须重命名所有的变量,为了这篇文章 http://disnetdev.com/blog/2013/09/27/hygiene-in-sweet.js/ 里面描述的所有的原因。不幸的是那意味着所有你的标识符,会变得丑一点,但是如果你传入 -r 参数 或者 "readable names"选项给 sweet.js 编译器,他会清理下,并且大部分情况都会重新变好看点。由于这仅仅在 es5 代码中生效,所以还不是默认开启的。
用 sweet.js 编译器
首篇教程仅需学会如何用在自己的项目上
npm install -g sweet.js 安装。现在 sjs 命令生效了,运行它用这些参数:
- -o 输出文件名,否则就打印到标准输出中了
- -c 生成一个 source map 方便 debug
- -m 导入一组逗号分隔的模块
- -r 用更适合阅读的变量名重命名变量 去除后面丑陋的 $1234 这种后缀,现在他只在 es5 下工作
如果你想编译一个使用宏的 foo.js 并且用上 source map,你需要 sjs -c -o foo.built.js foo.js 你能用一个不同的扩展名 例如 .sjs
我推荐使用 https://github.com/evanw/node-source-map-support 这个node模块,他自动处理错误信息,你只需要 require('source-map-support').install() 在文件顶部就可以啦;
Grunt
最后,你会想要一个 Grunt 任务,这更容易,甚至自动添加了 source-map 支持(需配置)。
npm install grunt-sweet.js --save-dev 然后写 Gruntfile.js 模仿我的示例项目 https://gist.github.com/jlongster/8838950
Gulp/Makefile/etc
https://github.com/sindresorhus/gulp-sweetjs gulp插件
第一部分教程结束
覆盖了用 sweet.js 写宏的基础,接下来的教程我会更深入,which I will release in a week or two. Follow my blog to see when that comes out. I am also speaking at MountainWestJS about macros March 17-18th so let me know if you'll be there!
(Thanks to Tim Disney, Nate Faubion, Dave Herman, and other who reviewed this)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。