3

sweet.js 带给javascript 类似Scheme and Rust语言中的卫生宏功能
宏允许你定制甜美的js语法,制成您梦寐以求的专用js:

macro def {
  case $name:ident $params $body => {
    function $name $params $body
  }
}

def add (a, b) {
  return a + b;
}

接下来是类的实现:

macro class {
  case $className:ident { 
    constructor $constParam $constBody
    $($methodName:ident $methodParam $methodBody) ... } => {

    function $className $constParam $constBody

    $($className.prototype.$methodName 
      = function $methodName $methodParam $methodBody; ) ...
  }
}

class Person {
  constructor(name) {
    this.name = name;
  }

  say(msg) {
    console.log(this.name + " says: " + msg);
  }
}
var bob = new Person("Bob");
bob.say("Macros are sweet!");

可以把macros看作是语法层面的函数. 像普通函数一样,你定义一个宏,然后用语法参数调用他,得到一个新的语法. 运行sweet.js宏,将会展开所有的宏定义,生成可运行在任何js环境中的原生js。

两种方式定义宏: 简单的基于模式匹配的规则宏 and 更牛的程式判断的宏(熟悉Scheme or Racket的话,他们分别对应着 syntax-rules 和 syntax-case).

基于模式的宏,规则:

Rule macros 原理:匹配一个语法模式,得到一个基于模板的新语法.

macro <name> {
  rule { <pattern> } => { <template> }
}
macro id {
  rule {
    // after the macro name, match:
    // (1) a open paren 
    // (2) a single token and bind it to `$x`
    // (3) a close paren
    ($x)
  } => {
    // just return the token that is bound to `$x`
    $x
  }
}
id (42)
// --> expands to
42

在模版中,$开头的模式匹配任意的 token,绑定到name上.

注意到:一个token包括逗号,而不仅仅是数字或者标识符,例如数组元素被认为是一个完成的token

id ([1, 2, 3])
// --> expands to
[1, 2, 3]

卫生宏如何保持干净

比如交换两个数的值

macro swap {
  rule { {$a <=> $b} } => {
    var tmp = $a;
    $a = $b;
    $b = tmp;
  }
}

var a = 10;
var b = 20;

swap {a <=> b}

以上被转化为

var a$1 = 10;
var b$2 = 20;

var tmp$3 = a$1;
a$1 = b$2;
b$2 = tmp$3;

加上类似$n的后缀,是为了避免下面的情况:

var tmp = 10;
var b = 20;

swap {tmp <=> b}

// --> naive expansion
var tmp = 10;
var b = 20;

var tmp = tmp;
tmp = b;
b = tmp;

有了卫生处理就不会弄错了:

var tmp = 10;
var b = 20;

swap {tmp <=> b}

// --> hygienic expansion
var tmp$1 = 10;
var b$2 = 20;

var tmp$3 = tmp$1;
tmp$3 = b$2;
b$2 = tmp$1;

想打破这个规则,可以看下一节;

模式和宏——天造地设的一对

解析类的使用:

macro m {
  rule { ($x:expr) } => {
    $x
  }
}
m (2 + 5 * 10)
// --> expands to
2 + 5 * 10

解析类目前支持:

  • expr—— 匹配一个表达式
  • ident—— 一个标识符匹配
  • ilt—— 匹配文字

可重复的模式:

macro m {
  rule { ($x ...) } => {
    // ...
  }
}
m (1 2 3 4)

要支持,? 加上(,)

macro m {
  rule { ($x (,) ...) } => {
    [$x (,) ...]
  }
}
m (1, 2, 3, 4)

用$()来支持模式组

macro m {
  rule { ( $($id:ident = $val:expr) (,) ...) } => {
    $(var $id = $val;) ...
  }
}
m (x = 10, y = 2+10)

多条规则也没问题

macro m {
  rule { ($x:lit) } => { $x }
  rule { ($x:lit, $y:lit) } => { [$x, $y] }
}

m (1);
m (1, 2);

规则按顺序直到匹配成功,于是宏可以递归定义啦:

macro m {
  rule { ($base) } => { [$base] }
  rule { ($head $tail ...) } => { [$head, m ($tail ...)] }
}
m (1 2 3 4 5)  // --> [1, [2, [3, [4, [5]]]]]

宏程式开始

更厉害的case macro可以用完全的js能力来处理语法:

macro <name> {
  case { <pattern> } => { <body> }
}

case macro 和rule macro不同之处,macro name也会被匹配到,而不光是其后的body内容:

macro m {
  case { $name $x } => { ... }
}
m 42  // `$name` will be bound to the `m` token
      // in the macro body

用_可以匹配任意的token,并忽略绑定。

macro m {
  case { _ $x } => { ... }
}

另一个不同之处是case macro的body部分包含一个模板和js的混合体,用来创建和操纵语法,下面是一个身份定义宏:

macro id {
  case {_ $x } => {
    return #{ $x }
  }
}

#{...}是syntax{...}的缩写,他创建一个【在模式匹配绑定作用域内的】语法对象数组

语法对象是sweet.js用于词法上下文【保持变量卫生】的声明式的token,可以用#{}模板创建,也可以用现有对象的词法上下文来创建独特的词法对象

macro m {
  case {_ $x } => {
    var y = makeValue(42, #{$x});
    return [y]
  }
}
m foo
// --> expands to
42

有以下内置的创建语法对象的函数

  1. makeValue(val, stx) – val 可以是 boolean, number, string, or null/undefined
  2. makeRegex(pattern, flags, stx) – pattern 是regex pattern的字符串表示 flags是regex flags的字符串表示
  3. makeIdent(val, stx) – val 是一个代表一个标识符identifier的字符串
  4. makePunc(val, stx) – val 是一个代表一个标点符号punctuation 的字符串 (e.g. =, ,, >, etc.)
  5. makeDelim(val, inner, stx) – val是分隔符 可以是"()", "[]", or "{}" 或者内部是一个语法对象syntax objects数组的所有tokens的内部分隔符.

(这些函数大致等于 Scheme/Racket 中的函数 datum->syntax)

如果要暴露语法对象在词法上下文中,以直接得到token,可以用unwrapSyntax(stx)(对应Scheme的syntax-e)。

用这些函数创建的新语法对象,很方便就可以在#{}模版中引用. 使用提供的letstx宏,绑定语法对象syntax objects到模式匹配变量:

macro m {
  case {_ $x } => {
    var y = makeValue(42, #{$x});
    letstx $y = [y], $z = [makeValue(2, #{$x})];
    return #{$x + $y - $z}
  }
}
m 1
// --> expands to
1 + 42 - 2

越来越脏,破坏卫生

打破卫生的方法是通过在“正确的位置”从语法对象上偷取词法上下文,To clarify, consider aif the anaphoric if macro that binds its condition to the identifier it in the body.

var it = "foo";
long.obj.path = [1, 2, 3];
aif (long.obj.path) {
  console.log(it);
}
// logs: [1, 2, 3]

This is a violation of hygiene because normally it should be bound to the surrounding environment ("foo" in the example above) but aif wants to capture it. To do this we can create an it binding in the macro that has the lexical context associated with the surrounding environment. The lexical context we want is actually found on the aif macro name itself. So we just need to create a new it binding using the lexical context of aif:

macro aif {
  case {
    // bind the macro name to `$aif_name`
    $aif_name 
    ($cond ...) {$body ...}
  } => {
    // make a new `it` identifier using the lexical context
    // from `$aif_name`
  var it = makeIdent("it", #{$aif_name});
  letstx $it = [it];
  return #{ 
      // create an IIFE that binds `$cond` to `$it`
      (function ($it) {
        if ($cond ...) {
          // all `it` identifiers in `$body` will now
          // be bound to `$it` 
          $body ...
        }
      })($cond ...);
    }
  }
}

让它成为…不递归

有时候不想要宏递归自身,比如要在函数执行前添加一些日志:

macro function {
  case {_ $name ($params ...) { $body ...} } => {
    return #{
      function $name ($params ...) {
        console.log("Imma let you finish...");
        $body ...
      }
    }
  }
}

他将永远循环下去,因为宏展开中的函数标识符绑定到了函数宏自身,阻止这个情况要使用let宏来绑定form表

let function = macro {
  case {_ $name ($params ...) { $body ...} } => {
    return #{
      function $name ($params ...) {
        console.log("Imma let you finish...");
        $body ...
      }
    }
  }
}

他将function绑定到了宏本体body,而不是宏本体body内部

带反查的宏,小心你的背后

你可以用中缀infix规则匹配之前的语法,用(|) 分离左右两边。

macro unless {
  rule infix { return $value:expr | $guard:expr } => {
    if (!($guard)) {
      return $value;
    }
  }
}

function foo(x) {
  return true unless x > 42;
  return false;
}

infix可以用于程式宏,宏名字是右侧第一个token

macro m {
  case infix { $lhs | $name $rhs } => { ... }
}

infix规则可以和正常的规则混合

macro m {
  rule infix { $lhs | $rhs } => { ... }
  rule { $rhs } => { ... }
}

infix尽力处理关闭之前的语法

macro m {
  rule infix { ($args ...) | $call:expr } => {
    $call($args ...)
  }
}

(42) m foo; // This works
bar(42) m foo; // This is a match error

Sweet.js目前支持声明式的宏定义,然而据Mozilla研究所的Tim Disney所说,计划将要支持命令式的定义。这意味着宏可以包含编译时运行的任意JavaScript代码。


tcdona
461 声望32 粉丝

may the money keep with you —— monkey


引用和评论

0 条评论