博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Sweet.js 教程 2 递归宏以及自定义模式类
阅读量:5944 次
发布时间:2019-06-19

本文共 8738 字,大约阅读时间需要 29 分钟。

from

在第一篇教程 翻译 。我们讲到基本的一些 sweet.js 概念。现在我们来看一些技术,来创建更复杂的宏:递归和自定义模式类。

所有的这些教程在 repo sweet.js-tutorials 包含了可以编译的 sweet.js 宏工作环境。

让我们创建一个 es6 非结构化变量 赋值特性宏。开始应该这样:

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 xvar x = arr[i++]// expanded with `var` macrovar x = arr[i++]// pattern class running: `destruct_array arr i y = 5`arr i y = 5var y = arr[i++] || 5// expanded with `var` macrovar 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://wnzxx.baihongyu.com/

你可能感兴趣的文章
Integer跟int的区别(备份回忆)
查看>>
集合解析
查看>>
详解分布式应用程序协调服务Zookeeper
查看>>
软件工程之构建之法
查看>>
UVa 10902
查看>>
Mathf.Sin正弦
查看>>
禁止浏览器缓存js
查看>>
【Redis】安装PHP的redis驱动(二)
查看>>
什么是序列化,为什么要序列化
查看>>
Java保留小数点后有效数字
查看>>
C++中一些类和数据结构的大小的总结
查看>>
mysql开启binlog
查看>>
ctrl + z fg bg
查看>>
工作流引擎Oozie(一):workflow
查看>>
struct框架
查看>>
Deep Learning(深度学习)相关网站
查看>>
设置Eclipse编码方式
查看>>
分布式系统唯一ID生成方案汇总【转】
查看>>
Cross-compilation using Clang
查看>>
营销系统--手动补偿
查看>>