HOME | EDIT | RSS | ARCHIVE | INDEX | ABOUT

入语言第二试: readtable 与 core.async

Star Vote on Hacker News

俺的小公举淼淼最近各种 发肚子拉烧 发烧拉肚子,难得抽点时间给入语言ru-lang加入俩个大大的 features,忍不住要 marketing 一下。

shit-bricks.gif

紧接上篇,在简单的介绍我是如何移植 clojure 的一些 macro 到 JavaScript 之后,我要介绍两个革命性的移植

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(可持久性数据结构)了。

这里引入的 mori 是我 fork 的版本(swannodette 好像最近忙着实现 om next 并没有心思维护 mori 的样子),暂且叫做 conjs,当然完全兼容 mori 重要的是我加入了一些其他 clojurescript 的函数以及 core.async,你可以通过 npm install con.js 安装使用。

core.async

首先,不知道这是什么的童鞋请回到这一篇,然后,感谢 clojurescript 的实现,使得这次移植这么顺利。懂得童鞋就会怀疑,clojurescript 不是还是用得 clojure 的 macro 来生成对应的状态机么?怎么可能轻松移植到 javascript?

但是,我真的只加了几行代码就移过来了(此处掌声)

applause.jpg

不信请看 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();
        });
    });
}());

what.gif

没错,把 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

也是极好的.