JavaScript玩转Clojure大法之 - Macro (1)
- JavaScript玩转Clojure大法之 - 并发编程
- JavaScript玩转Clojure大法之 - Transducer
- JavaScript玩转Clojure大法之 - Trampoline
- JavaScript玩转Clojure大法之 - Macro (1)
macro可以说是lisp语言的独门绝技, lisp语言数据即代码,以及s-expression的特点使得可以轻松自定义macro. 虽然js原生不能这么玩, 但是依然不能阻止我们通过sweet.js在预编译的过程中使用macro.
Macro
我非常不喜欢中文字面翻译–'宏', 中文宏的意思是大, 广大, 实在想不通这跟macro有毛关系. 而macro值的是某条指令可以扩展 成一堆其它指令. 反而用个图我觉得更贴切
(macroexpand '(when-not (= 1 3) (print "damn")))
; => (if (= 1 3) nil (do (print "damn")))
看看Clojure的macro when-not
能扩展成什么? 神奇的变成了if.
靠, 这不是就是语法糖?
嗯, 就是语法糖, 但是是强大的可以自定义的语法糖. 这代表着你可以用Clojure编写自己独特版本的Clojure.
lisp语言因为本身的原因能轻松这么办到
- 语法简单,只有s-expression
- 数据即代码, s-expression本身就是树形数据结构
- lexer(词法分析器) → Reader → expander
如果英文好, 可以看看这篇解释clojure macro的文章
但是就算可以自定义语法糖, 到底有什么好处呢, 真的只是使语法更好看吗?
当然不是, macro可以说是元编程的终极形态, 当Clojure推出core.async这么牛逼的库之后, 立即就被port到 ClojureScript, 也就是说, ClojureScript写的 go block 可以编译成能在浏览器上抛的单线程JavaScript.
如果回忆不起来可以翻看下如何用JavaScript实现 core.async 的 go block. 你会发现 generator 是实现的关键, 而ClojureScript却只用macro展成不同的纯状态机实现.
怎么做到的呢, 就是macro, 如果你翻看ClojureScript 的 core.async源码, 会看见一堆一堆的macro. 根据go block 中不同的语法扩展成不一样的状态机.
Sweet.js
因此对于像其它有更多语法的语言要实现macro可就没那么简单了(虽然一些新的语言还是很努力的实现了macro, 比如rust和julia). 比较直白的实现方式是定义的macro接收一个 AST, 然后改吧改吧, 做macro该干得事情. 但是操作语法树实在是太复杂了, 跟自己写类似coffeescript编译器有毛区别.
而sweet.js 给我提供了一个自制js macro的工具, sweet.js来自mozilla
mozilla还有论文呢: https://github.com/mozilla/sweet.js/blob/master/doc/paper/sweetjs.pdf
, 嗯嗯, 就是rust的那个mozilla,
就是那个 如果没有chrome, 应该能占浏览器半壁江山的 汪峰 firefox
的mozilla 公司 基金会.
Rule macro
因此sweet.js和mozilla自己的语言rust支持的macro语法上非常接近, 也绝逼不是巧合.
来看看rust的 rule macro
macro_rules! foo {
(x => $e:expr) => (println!("got a ", $e));
}
foo!(x => 3); // => println!("got a ", 3)
来看看 Sweet.js的 rule macro
macro foo {
{
(x=>$e:expr)
} => {
console.log('got a ', $e)
}
}
foo(x=>3) // => console.log("got a ", 3)
简直是一模一样, 好吧, 我承认我的标题应该改成 javascript玩转rust大法更为贴切, 但是我们先来关心下这里面到底发生了什么?
实际上sweet.js做了之前clojure的Reader 和 Expander的工作
- lexer 把源码token化, 得到token序列
- Reader把token编程token树, 这是一个类似 sexpr 的token树
- 变term树
- Expander按照定义的macro匹配扩展token树, 再parse成AST
说人话!
拿 macro foo 作为例子
- 变token: foo • ( • x • = • > • 3 • )
- 变token树: 括号里面是一棵树, 第一个是根, 后面的元素有括号的是子树, 没有的就是叶子了.
(foo ('()' x = > 3))
变term树:
(call:foo x = > 3)
- expand:
(call:foo (call:console.log 'got a' , 3))
这个, 这个这个……怎么说好变成树怎么就变成lisp了
没错, lisp 简单的 s-expr 界限非常清晰而且本身就是完美的树型结构, 实现macro最方便的方式
case macro
Allright, 当然这个例子好简单, 但是像 rule macro 只能做一些非常简单的形式上的一一变化, 那么说好的元编程呢? 说好的可以像clojure那样用clojure编写clojure代码呢. 这时候case macro就是解决这个问题了. clojure由于 数据即代码, 代码只要quote起来就跟list一样好操作, 那么JavaScript麻烦的语法要怎么变数据好让我们用JavaScript操作呢?
答案是太复杂不能变数据, 但是只能变AST, 只能操作复杂的语法树了, 真是忧伤, 但是总比没有好吧.
让我们先来一 发 个例子
macro m {
case {ctx (x=>$x)} => {
console.log('haha iam javascript')
return #{
console.log($x)
}
}
}
m(100)
//=> haha iam javascript (to console)
//=> console.log(100)
如果你已经忘了, 请 电梯 返回去对比一下到底有什么区别
- 多了一个参数
ctx
, 匹配用到m时的那个m - 接下来都一样, 直到…
#{}
这个是什么? 这里面的语法变成语法树, 当然语法树结构是数组, 每个元素还是一个token树.比如console.log(3)大概是这种结构
[
{token: {value: 'console'}}
{token: {value: '.'}},
{token: {value: 'log'}},
{token: {value: '()'},
inner:[
{token: {value: 3}}
]}
]
- 最重要的, 现在里面可以写正常js了, 意味着你可以用js编程各种语法,然后拼到token树中
我感觉语言以及不能解释了, 请深吸一口气, 来一个骚味复杂一点的栗子
比如我要在js里弄一个想clojure的arity function一样骚的函数
arity function指根据不同个数的参数, 有不同的函数body. 比如
(defn add ([x] (+ 0 x)) ([x y] (+ x y))) (add 1);=>2 (add 1 2);=>3
所以类似的我期望的能在js里这样定义函数
defn add {
(a){a}
(a, b) {a+b}
}
add(1) //=> 1
add(1, 2) //=> 3
先把macro摆出来
//var macro from http://jlongster.com/Sweet.js-Tutorial--2--Recursive-Macros-and-Custom-Pattern-Classes
macro caseFunc {
case {_ ($args...) {$body... $last:expr}} =>
{
letstx $len = [makeValue(#{$args...}.length , null)];
return #{
case $len:
return (function($args...){$body... return $last}).apply(this, arguments)
}
}
}
macro defn{
rule { $name { $(($args (,) ...){$body ...})...} } => {
function $name (){
switch(arguments.length){
$(caseFunc ($args...) {$body...};
)...
}
}
}
}
defn arity_function{
(a){a}
(a, b) {a + b}
}
// =>
/*
function arity_function() {
switch (arguments.length) {
case 1:
return function (a) {
return a;
}.apply(this, arguments);
case 2:
return function (a, b) {
return a + b;
}.apply(this, arguments);
}
}
*/
WTF shen me gui
叫我一点一点解释, 重要的是第二个macro(第一个应该都能看懂吧), 这里面有几个新东西
- 第2行的
$last:expr
: 匹配最后一个表达式 - 第4行: 里面的
#{$args}
把match到的javascript语法变成token树的列表. - 这个token列表就是javascript的数组, 里面是token对象.
- 用
makeValue
把这个javascript再变成token树 - 用
letstx $len
来装这个token树, 就可以在后面的#{}
- 用
- 最后返回token树
1: macro caseFunc {
2: case {_ ($args...) {$body... $last:expr}} =>
3: {
4: letstx $len = [makeValue(#{$args...}.length , null)];
5: return #{
6: case $len:
7: return (function($args...){$body... return $last}).apply(this, arguments)
8: }
9: }
10: }