Sweet.js 教程 2 遞歸宏以及自定義模式類

from http://jlongster.com/Sweet.js-Tutorial--2--Recursive-Macros-and-Custom-Pattern-Classesphp

在第一篇教程 http://jlongster.com/Writing-Your-First-Sweet.js-Macro 翻譯 http://segmentfault.com/blog/tcdona/1190000002490290 。咱們講到基本的一些 sweet.js 概念。如今咱們來看一些技術,來建立更復雜的宏:遞歸和自定義模式類。git

全部的這些教程在 repo sweet.js-tutorials https://github.com/jlongster/sweet.js-tutorials 包含了能夠編譯的 sweet.js 宏工做環境。es6

讓咱們建立一個 es6 非結構化變量 http://wiki.ecmascript.org/doku.php?id=harmony:destructuring 賦值特性宏。開始應該這樣:github

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 非結構化變量賦值,處理了不少複雜的狀況。segmentfault

  • 對象/哈希: var {foo, bar} = obj;
  • 默認值: var [foo, bar=5] = arr;
  • 重命名: var {foo: myFoo} = obj;
  • 混合的解構: var {foo, bar: [x, y]} = obj;

你能夠用學到的全部概念,把上面的宏寫出來,可是當你嘗試支持更繽紛的語法時,你可能被卡住。如今咱們來看看一些技術來處理更復雜的狀況。數組

遞歸宏

我在前一篇教程小提過遞歸宏,它們指的更深刻的研究,以用來解決實際問題。dom

宏的輸出老是被 sweet.js 再次展開,因此寫遞歸宏和寫遞歸函數是同樣天然的。只要一個規則 rule 再次調用宏,而後其餘的規則匹配了這個暫停的狀況,而且中止本次展開。ecmascript

一個普通的遞歸宏用例是處理一組不統一的語法。一般用重複格式 $item (,) ...匹配一組語法,用組 $($item = $arr[$i]) ... 甚至能夠匹配複雜的。問題是每一個組中的元素必須是一樣的結構。你不能匹配語法類型不一樣的組。 sweet.js 沒有相似正則重的或 | 操做符,或者可選擇的 ? 符號。編輯器

例如,咱們想匹配一組名字,可能含有初始化賦值語句: x, y, z=5, w=6。 咱們想迭代 items 並造成不一樣的代碼若是初始化賦值語句存在的話。來看用遞歸宏怎麼作:ide

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 ,爲了之後的教程。

相關文章
相關標籤/搜索