

JavaScript玩转Clojure大法之 - Macro (1)
source link: https://blog.oyanglul.us/javascript/clojure-essence-in-javascript-macro
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JavaScript玩转Clojure大法之 - Macro (1)
Table of Contents
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: }
Recommend
-
74
走近WebAssembly之调试大法
-
93
程序员 - @Antidictator - ### 挑花眼的过程:* B 站的评测都是 5-10K 档次。* 看了张大妈的推送好几周了,评论一直解毒: * 1G 内存,pass * 做过康佳的兼职不发工资所以不买康佳的
-
51
随着数据集和深度学习模型的规模持续增长,训练模型所需的时间也不断增加,大规模分布式深度学习结合数据并行化是大幅减少训练时间的明智选择。然而,在大规模 GPU 集群上的分布式深度学习存在两大技术难题。第一大难题是大批量训练下的...
-
52
本文笔者将从销售环节切入,带你一窥教育行业的全貌。 本文受个人视角局限,不足之处诸君多多指正。 天下生意首要是能卖出去,教育行业是咋卖课的? 王二蛋这天面试到了一家公司,说是当课程顾问。报到之后才知道工作内容就是卖课程的,二蛋心理很不屑,不就是卖东...
-
37
只要29800,“全脑教育”包终生
-
12
JavaScript玩转Clojure大法之 - Trampoline JavaScript玩转Clojure大法之 - Trampoline Table of Contents 📣 不要再找了, 弹幕功能已关闭 在函数式编程中...
-
6
JavaScript玩转Clojure大法之Transducer JavaScript玩转Clojure大法之Transducer Table of Contents 📣 不要再找了, 弹幕功能已关闭 通过上一篇
-
10
Custom defn macro with clojure.spec - part 1: conform/unform Oct 10, 2016 • Yehonathan Sharvit Parsing With clojure.spec, we can parse functions and macro...
-
8
自制语言初试 - 入lang Table of Contents Vote on Hacker News
-
1
JavaScript玩转Clojure大法之 - Macro (1) JavaScript玩转Clojure大法之 - Macro (1) Table of Contents macro可以说是lisp语言的独门绝技, lisp...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK