入语言第二试: 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也是极好的.