3

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)


tcdona
461 声望32 粉丝

may the money keep with you —— monkey


引用和评论

0 条评论