from http://jlongster.com/Sweet.js-Tutorial--2--Recursive-Macros-and-Custom-Pattern-Classes
在第一篇教程 http://jlongster.com/Writing-Your-First-Sweet.js-Macro 翻译 http://segmentfault.com/blog/tcdona/1190000002490290 。我们讲到基本的一些 sweet.js 概念。现在我们来看一些技术,来创建更复杂的宏:递归和自定义模式类。
所有的这些教程在 repo sweet.js-tutorials https://github.com/jlongster/sweet.js-tutorials 包含了可以编译的 sweet.js 宏工作环境。
让我们创建一个 es6 非结构化变量 http://wiki.ecmascript.org/doku.php?id=harmony:destructuring 赋值特性宏。开始应该这样:
let var = macro {
rule { [$var (,) ...] = $obj:expr } => {
var i = 0;
var arr = $obj;
$(var $var = arr[i++]) (;) ...
}
rule { $id } => {
var $id
}
}
var [foo, bar, baz] = arr;
var i = 0;
var arr$2 = arr;
var foo = arr$2[i++];
var bar = arr$2[i++];
var baz = arr$2[i++];
这仅仅处理了基础的简单数组。我们把目标对象分配给 arr 确保表达式只执行了一次。 es6 非结构化变量赋值,处理了很多复杂的情况。
- 对象/哈希: var {foo, bar} = obj;
- 默认值: var [foo, bar=5] = arr;
- 重命名: var {foo: myFoo} = obj;
- 混合的解构: var {foo, bar: [x, y]} = obj;
你可以用学到的所有概念,把上面的宏写出来,但是当你尝试支持更缤纷的语法时,你可能被卡住。现在我们来看看一些技术来处理更复杂的情况。
递归宏
我在前一篇教程小提过递归宏,它们指的更深入的研究,以用来解决实际问题。
宏的输出总是被 sweet.js 再次展开,所以写递归宏和写递归函数是一样自然的。只要一个规则 rule 再次调用宏,然后其他的规则匹配了这个暂停的情况,并且停止本次展开。
一个普通的递归宏用例是处理一组不统一的语法。通常用重复格式 $item (,) ...匹配一组语法,用组 $($item = $arr[$i]) ... 甚至可以匹配复杂的。问题是每个组中的元素必须是同样的结构。你不能匹配语法类型不同的组。 sweet.js 没有类似正则重的或 | 操作符,或者可选择的 ? 符号。
例如,我们想匹配一组名字,可能含有初始化赋值语句: x, y, z=5, w=6。 我们想迭代 items 并形成不同的代码如果初始化赋值语句存在的话。来看用递归宏怎么做:
macro define {
rule { , $item = $init:expr $rest ... ; } => {
var $item = $init;
define $rest ... ;
}
rule { , $item $rest ... ; } => {
var $item;
define $rest ... ;
}
rule { ; } => { ; }
rule { $items ... ; } => {
define , $items ... ;
}
}
define x, y, z=5, w=6;
var x;
var y;
var z = 5;
var w = 6;
;
当使用了递归宏,你需要考虑到边缘的情况,比如后面的逗号。因为我们匹配的是逗号分割的组,我们需要剥开逗号,但是我们不能一直有逗号,因为最后的元素没有。我们解决这个问题依靠在组的开头加一个逗号,然后当迭代器穿过这个组的时候剥开逗号。因为初始的调用不会有逗号在前面,所以会匹配到最后一个规则,添加逗号并且递归调用。
当没到达最后的元素了,仅剩下 ; 了,因此它匹配了规则 3,只是输出 ; 然后停止迭代;
现在是提醒你,你可以在在线编辑器中使用 "step" 调试宏展开的好时候。当调试递归宏的时候 step 非常好用。你能看到他是如何一片一片展开的。
现在你看到了展开是如何在我们控制下展开的。我们来看看更复杂的栗子。我们试着加一个特性给我们的原生非结构化宏:指定默认的值的能力。你可以用一个逗号分开的变量名的数组表格,加上一个可选的 = 来定制元素的默认初始值,如果元素不存在的话。 var [foo, bar=5] = ... 和 var [foo, bar=5, baz] = ... 都是合法的。
先来回一下,我们前面教程举的 let 宏例子, let var = macro { ... }。记得吗?他告诉 sweet.js 任何我们自己展开形成的 var 不应该递归的被展开
我们需要创建一个能被递归的辅助宏,因为我们不能让 var 递归。看我们如何来实现非结构化赋值,带可选的初始化表格:
macro destruct_array {
rule { $obj $i [] } => {}
rule { $obj $i [ $var:ident = $init:expr, $pattern ... ] } => {
var $var = $obj[$i++] || $init;
destruct_array $obj $i [ $pattern ... ]
}
rule { $obj $i [ $var:ident, $pattern ... ] } => {
var $var = $obj[$i++];
destruct_array $obj $i [ $pattern ... ]
}
}
let var = macro {
rule { [ $pattern ...] = $obj:expr } => {
var arr = $obj;
var i = 0;
destruct_array arr i [ $pattern ... , ]
}
rule { $id } => {
var $id
}
}
var [x, y] = arr;
var [x, y, z=10] = arr;
var arr$2 = arr;
var i = 0;
var x = arr$2[i++];
var y = arr$2[i++];
;
var arr$3 = arr;
var i$2 = 0;
var x = arr$3[i$2++];
var y = arr$3[i$2++];
var z = arr$3[i$2++] || 10;
;
var 宏返回了一个语法,它包含非结构化的数组 destruct_array 宏,sweet.js 递归的展开它。这有点难懂,但不是太坏,我们走读一下:
- destruct_array 是递归宏,一次只展开成数组的一项元素。它匹配第一个元素,生成代码,然后用接下来的元素触发又一个 destruct_array 的调用。当没有元素可以扩展了,他就停下来
- 在 var 宏中,我们添加了一个扩展的逗号在组结束的时候,这让他很容易 destruck_array ,选取第一个元素,因为它可以匹配到后面的逗号
- $pattern ... 匹配了 0 或者多个元素,因此 [$var, $pattren] 将匹配最后的元素 [x, ],剥开他然后 [] 会匹配停止递归的规则
- 我们不用 $pattern (,) ... 虽然我们匹配了逗号分隔符。我们做的是匹配 $pattern ... 而不是他的元素,因此我们匹配了所有的东西包括逗号
这里是 var [x, y=5] = expr 的展开:
var [x, y=5] = expr;
var arr = expr;
var i = 0;
destruct_array arr i [ x , y = 5 , ];
var arr = expr;
var i = 0;
var x = arr [ i ++ ];
destruct_array arr i [ y = 5 , ];
var arr = expr;
var i = 0;
var x = arr [ i ++ ];
var y = arr [ i ++ ] || 5;
destruct_array arr i [ ];
var arr = expr;
var i = 0;
var x = arr [ i ++ ];
var y = arr [ i ++ ] || 5;
值得注意的是 js 中有几个地方你不能调用宏。如果用递归宏你得注意这点。例如你不能在 var 绑定或者函数变量名字中调用宏。
var invoke_macro() { do_something_weird } 不工作, function foo (invoke_macro{}) {} 也不会工作。
意思是说你不能这样:
macro randomized {
rule { RANDOM $var } => {
$var = Math.random()
}
rule { $var (,) ...; } => {
var $(randomized RANDOM $var) (,) ...
}
}
randomized x, y, z;
Error: Line 11: Unexpected identifier [... var randomized RANDOM x , ...]
去掉 var 才行的通。你想要在规则内部有本地展开语法的能力,但是 sweet.js 还不支持这点。
理论上我们的宏应扩展成单独一个 var 语句,比如 var arr = expr, i=0, x = arr[i++] 代替多个 var 声明。我们现有的宏因为在 for while 语句里面( for(var [x, y] = arr; x<10; x++){} )因为多行语句在这里是无效的。不幸的是,我们需要递归的在 var 中调用一个宏,并绑定一个位置,但如上面的规则我们不能这么做。宏会展开的类似 var destruct_array arr i [$pattern ..., END] 但是你不能这么做。
我们继续解构宏,并加上混合解构支持。你应该能用 var [x, [y, z]] = arr 但是我们的宏不能处理这个。用递归宏,我们可以非常简单的添加这点。我们需要的只是让 destruct_array 能接受任何类型的口令( $var:id 被改成了 $first )并且转换宏的顺序
let var = macro {
rule { [ $pattern ...] = $obj:expr } => {
var arr = $obj;
var i = 0;
destruct_array arr i [ $pattern ... , END ]
}
rule { $id } => {
var $id
}
}
macro destruct_array {
rule { $obj $i [ END ] } => {
}
rule { $obj $i [ $var:ident = $init:expr, $pattern ... ] } => {
var $var = $obj[$i++] || $init;
destruct_array $obj $i [ $pattern ... ]
}
rule { $obj $i [ $first, $pattern ... ] } => {
var $first = $obj[$i++];
destruct_array $obj $i [ $pattern ... ]
}
}
var [x, y] = arr;
var [x, [y=5, z]] = arr;
var arr$2 = arr;
var i = 0;
var x = arr$2[i++];
var y = arr$2[i++];
;
var arr$3 = arr;
var i$2 = 0;
var x = arr$3[i$2++];
var arr$4 = arr$3[i$2++];
var i$3 = 0;
var y = arr$4[i$3++] || 5;
var z = arr$4[i$3++];
;
;
我们改变了 var 和 destruct_array 的顺序,因为我们用 var 在 destruct_array 中来创建新的标识符并且用右边的元素初始化他们。如果“元素”是另一个模式,比如 [y, z] 我们想要结构它。我们难道不能用 var 宏来递归的解构他吗?是的我们可以!现在, let 宏只是在他定义之后生效,因此如果我们定义 destruct_array 在后面他会递归的展开进入它。
递归宏给了我们更多控制扩展的能力。我们留下一个解构给大家( var {x, y: foo} = obj )当递归性用熟了,我们看看另外一种匹配复杂的模式的姿势,它直觉上更容易用。
自定义模式类
第一篇教程我提到模式类告诉扩展器匹配什么类型的口令。 indet, lit 和 expr 是 sweet.js 内建的。实际上你可以自定义模式类来抽象任何复杂的模式匹配。
常见的问题是需要思考如何抽象,这很必要,尤其是在匹配重复性的模式的时候。递归宏允许你建立辅助宏来建立抽象层。自定义模式类也允许你这样,但是他更(自然/表象/直觉) intuitive。
自定义模式类非常简单:只需要弄个宏!宏可以被作为模式类调用。(弄一个宏 foo, 用 rule { $x:foo } => {})。这里你也有2个表格 forms 可以用: $x:invoke(foo) 和 $x:invokeOnce(foo) 。invoke 递归的展开 foo 宏的结果, invokeOnce 只展开一次。 $x:foo 是 $:invoke(foo) 的简写。
来看我们之前做的递归 define 宏,但是用了模式类代替:
macro item {
rule { $item = $init:expr } => {
var $item = $init
}
rule { $item } => {
var $item
}
}
macro define {
rule { $items:item (,) ... ; } => {
$items (;) ...
}
}
define x, y, z=5, w=6;
var x;
var y;
var z = 5;
var w = 6;
一个模式类在口令流上运行,然后替换成宏展开后的样子。 item 宏返回 var 定义,我们只需要在 define 中输出 $items。模式类在许多情况下比递归模式简单。因为你不需要记账是的处理尾部的逗号什么的。
如果 item 返回了一个宏作为第一个口令,他会不断的展开。任何宏中的其他代码都没机会被展开了。$items:invoke(item) 或者说 $items:item 的递归性只集中在 “头部” 。如果你不想这样,用 $items:invokeOnce(item) 来从最初的匹配中回来。
我们的解构宏看起来会变成怎样,如果我们用模式类代替递归宏?:
let var = macro {
rule { [ $pattern:destruct_array (,) ...] = $obj:expr } => {
$pattern (,) ...
}
rule { $id } => {
var $id
}
}
问题是我们需要给 destruct_array 传递参数。我们转换组中的元素,来让每个元素包含参数,然后用一个辅助宏来触发这个模式类
let var = macro {
rule { [ $pattern:expr (,) ...] = $obj:expr } => {
var arr = $obj;
var i = 0;
destruct [ $(arr i $pattern) (,) ... ]
}
rule { $id } => {
var $id
}
}
macro destruct {
rule { [ $pattern:destruct_array (,) ... ] } => {
$pattern (;) ...
}
}
我们建立了需要传给解构状态的 arr 和 i 变量,然后建立了一组元素 destruct 可以用 destruct_array 来组合。现在我们只需要定义 destruct_array。完整的来了:
let var = macro {
rule { [ $pattern:expr (,) ...] = $obj:expr } => {
var arr = $obj;
var i = 0;
destruct [ $(arr i $pattern) (,) ... ]
}
rule { $id = $init:expr } => {
var $id = $init
}
rule { $id } => {
var $id
}
}
macro destruct_array {
rule { $obj $i $var = $init:expr } => {
var $var = $obj[$i++] || $init
}
rule { $obj $i $var } => {
var $var = $obj[$i++]
}
}
macro destruct {
rule { [ $pattern:destruct_array (,) ... ] } => {
$pattern (;) ...
}
}
var [x, y] = arr;
var [x, y, z=10] = arr;
var [x, [y, z=10]] = arr;
var arr = arr$4;
var i = 0;
var x = arr[i++];
var y = arr[i++];
var arr$2 = arr$4;
var i$2 = 0;
var x = arr$2[i$2++];
var y = arr$2[i$2++];
var z = arr$2[i$2++] || 10;
var arr$3 = arr$4;
var i$3 = 0;
var x = arr$3[i$3++];
var arr$4 = arr$3[i$3++];
var i$4 = 0;
var y = arr$4[i$4++];
var z = arr$4[i$4++] || 10;
它支持初始化表格( var [x=5] = arr )并且混合解构。这里如何混合解构真的很有趣:destruct_array 生成的 var 被我们的宏引用,因此他可以递归展开。
递归性任然在模式类中有效,但你得小心翼翼的。var 宏返回的东西会注入到 destruct 的匹配中。注意我们如何在 var 中添加一个规则来匹配 $id = $init:expr 表格。我们需要这点,这样它才能在递归展开的时候返回整个的表达式给 destruct。
现在你不能单步的调试模式类的展开,但是它张这样:
var [x, y=5] = expr;
var arr = expr;
var i = 0;
destruct [ arr i x , arr i y = 5 ]
// pattern class running: `destruct_array arr i x`
arr i x
var x = arr[i++]
// expanded with `var` macro
var x = arr[i++]
// pattern class running: `destruct_array arr i y = 5`
arr i y = 5
var y = arr[i++] || 5
// expanded with `var` macro
var y = arr[i++] || 5
// back inside `destruct`
var x = arr[i++];
var y = arr[i++] || 5;
现在这个宏可以做我们递归宏可以做的任何事情了,而且他更清晰、干净。他还让我们更接近产生一个 var 语句的能力 比如 var arr = expr, i=0, x=arr[i++] 因为模式类让我们能重复。表格 var $el (,) ... 是盒饭的因为它在返回给解析之前展开了; 你只是不能在 var 绑定之中递归展开。
不幸的,由于我们需要建立2个新的绑定 arr 和 i ,我们无法生成单独的 var 声明了。var 宏产生了这些绑定,然后调用 destruct 宏,因此宏调用不会再 var 绑定的内部发生。想要生成一个单独的简单干净的 var 声明的唯一方式是让我们在宏规则中有生成本地展开语法的能力,但是我们还没发支持这点。
第二部分结束
你能用这2中技术创建很多有趣的宏。未来我们会涉及像 infix 宏, case 宏,等,保持协调,并且关注我的博客 http://feeds.feedburner.com/jlongster ,为了以后的教程。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。