入语言第二试: readtable 与 core.async
俺的小公举淼淼最近各种 发肚子拉烧 发烧拉肚子,难得抽点时间给入语言ru-lang加入俩个大大的 features,忍不住要 marketing 一下。
紧接上篇,在简单的介绍我是如何移植 clojure 的一些 macro 到 JavaScript 之后,我要介绍两个革命性的移植
- readtable
- core.async
Readtable
readtable 在 clojure 中的意义是说 macro 可以按照几种 readtable 进行扩展,比如遇到特殊符号‘#’,就可以用另一张 table 中的 macro 来扩展了。
(+ 1 2) ; a list
#(+ 1 2) ; => (fn (+ 1 2))
{:a 1 :b 2} ; hash map
#{:a 1 :b 2} ; set
由于 sweet.js 也支持 readtable,虽然并不是很完美,我就尝试了一下让 ru-lang 也能在遇到‘#’的时候做一些特俗的扩展。比如这是我想要实现的功能,让这几种 literal 的写法遇到‘#’后扩展成 mori 对应的数据结构:
#[bar, he] // => mori.vector(bar,he)
#{a: 1, b: 2} //=> mori.hashMap('a', 1, 'b', 2)
##{1, 2, 3} //=> mori.set([1,2,3])
我还抽空做了一个 ru-lang repl, 所有 ru-lang 都可以在这里试运行 http://ru-lang.org/try/
实现再简单不过了,先创建一个 readtable,挂上‘#’
sweet.currentReadtable().extend({
'#': function(ch, reader) {
...
}
})
以 vector 为例,当遇到‘#’后边为 ()
时,将它变成 mori.vector…就好了:
module.exports = sweet.currentReadtable().extend({
'#': function(ch, reader) {
var hashtag = reader.readIdentifier();
var pun = reader.readToken();
switch(pun.value){
case '[]':
return [reader.makeIdentifier('mori.vector')].concat(
reader.makeDelimiter('()',pun.inner)
);
...
这样一来,我们就可以像原生 literal 创建数据结构一样使用到 mori 的persistent data structure(可持久性数据结构)了。
core.async
首先,不知道这是什么的童鞋请回到这一篇,然后,感谢 clojurescript 的实现,使得这次移植这么顺利。懂得童鞋就会怀疑,clojurescript 不是还是用得 clojure 的 macro 来生成对应的状态机么?怎么可能轻松移植到 javascript?
但是,我真的只加了几行代码就移过来了(此处掌声)
不信请看 https://github.com/jcouyang/conjs/commit/aaf843d3a1c8cf97ff8d453242fe5ea4a213a9e2
移植了以后看我怎么用(更多测试在这里)
var c = async.chan()
async.take$(c ,function(x){
expect(x).toBe('something in channel')
done()
})
async.put$(c, 'something in channel')
等一下,这怎么是回调的 take, go block
在哪里? <! >!
在哪里?
那些都是 macro,当然我还要实现对应的 macro 了,先来看下加了 go block macro 后的效果:
var channel1 = mori.async.chan();
var channel2 = mori.async.chan();
var data2 = [1,2,3];
go {
var a <! channel1;
var b <! channel2;
expect(a).toBe("data1");
expect(b).toEqual([1,2,3]);
done();
};
go {data2 >! channel2};
go {'data1' >! channel1};
当然还支持 alts
var channela = mori.async.chan();
var channelb = mori.async.chan();
var data2 = [1,2,3];
go {
var anywho <!alts [channela, channelb];
// vector.a(0) is equals to nth(vector, 0)
expect(anywho.a(0)).toEqual([1,2,3]);
expect(anywho.a(1)).toBe(channelb);
done();
};
go {data2 >! channelb};
go {'data1' >! channela};
go block macro 的实现其实也没有花太多的代码, 以 take 为例,只需要把后面的句子都放入 take 的 callback 中好了,通过我的 sweet macro 简介 我想这里应该能看懂的:
let (<!) = macro {
rule infix { var $left:ident | $right:expr $rest $[...] } => {
return mori.async.take$($right, function (value) {
$left = value
$rest $[...]
})
}
...
}
- 一个 infix macro,左边是take应该付给的变量,右边是 take 的 channel
- 剩下的 body 直接全丢到 take 的 callback 中。
所以,上面的 take 测试放到 ru-lang repl 中会编译成
go {
var a <! channel1;
var b <! channel2;
expect(a).toBe("data1");
expect(b).toEqual([1,2,3]);
done();
};
// =>
(function () {
return mori.async.take$(channel1, function (value) {
a = value;
return mori.async.take$(channel2, function (value$2) {
b = value$2;
expect(a).toBe('data1');
expect(b).toEqual([
1,
2,
3
]);
done();
});
});
}());
没错,把 core.async 移植到 javascript,即不需要 ES6 的 generator, 也不用等 ES7 的 async function,更不需要任何生成状态机的 macro。简简单单的 callback + macro + clojurescript core.async channel,就这么简单, 实现任何浏览器都能用的 core.async go block。
这样,通过 ru-lang,可以让 javascript 轻松使用到 clojure 的 persistent data structure,还可以用 clojurescript 的 core.async。
最后,小广告
如果对这个项目有兴趣, 不妨接着在hacker news
Vote on Hacker News上讨论或 vote, 或者帮我在github上再加颗星
Star也是极好的.