sweet.js 帶給javascript 相似Scheme and Rust語言中的衛生宏功能
宏容許你定製甜美的js語法,製成您求之不得的專用js:javascript
macro def { case $name:ident $params $body => { function $name $params $body } } def add (a, b) { return a + b; }
接下來是類的實現:java
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。git
兩種方式定義宏: 簡單的基於模式匹配的規則宏 and 更牛的程式判斷的宏(熟悉Scheme or Racket的話,他們分別對應着 syntax-rules 和 syntax-case).github
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上.ruby
注意到:一個token包括逗號,而不單單是數字或者標識符,例如數組元素被認爲是一個完成的tokenless
id ([1, 2, 3]) // --> expands to [1, 2, 3]
好比交換兩個數的值ide
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的後綴,是爲了不下面的狀況:ui
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
解析類目前支持:
可重複的模式:
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
有如下內置的建立語法對象的函數
(這些函數大體等於 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代碼。