在经历了 JS 的 callback hell 后看过 Promise
, 于我而言这仍不能很好解决一些特定用况, 加之其写起来仍然很冗长, 各种匿名函数横飞. 于是就没有继续使用了, 但又不方便说 "反感 Promise" 或 "对 Promise 底层了如指掌" 否则就 mingge 体了. 总之千言万语汇成一句话
任何用库去解决语言本身存在的问题都是徒劳的.
那么就来造了轮子来解决它 :)
Flatscript 的解决方案
编译时语法树更改
Flatscript 并不依赖于 promise 体系, 而是直接修改语法树. 比如下面的 Flatscript 代码
console.log(fs.readFile('a.txt', %%))
的编译结果等价于以下 JS
fs.readFile('a.txt', function(err, result) { if (err) throw err; console.log(result);})
源代码中 fs.readFile
的最后一个参数是两个百分号, 这是一个标记参数, 指明函数调用的此参数是一个回调, 那么编译器在处理这一段代码时, 会将语法树中该调用表达式所在的语句中其他成分生成到回调函数的函数体中去.
简单示例解释所谓 "语法树中的其他成分", 譬如如下的语句
console.log(fs.readFile('a.txt', %%) + fs.readFile('b.txt', %%))
此语句中的表达式有 4 个主要表达式, 递归地从叶到根 (编译顺序亦是如此) 是
- 异步函数调用
fs.readFile
, 参数'a.txt'
(另一参数是异步标记就不列出了) - 异步函数调用
fs.readFile
, 参数'b.txt'
- 双目运算
+
, 作用于表达式 1 和表达式 2 - 普通函数调用
console.log
, 参数为表达式 3
对于表达式 1 这个异步调用而言, 表达式 2/3/4 便是语法树中的其他成分; 类似地对于表达式 2, 表达式 1/3/4 是语法树中的其他成分. 也就是说, 编译器处理完表达式 1 后, 会生成一个回调函数, 函数体中包含表达式 2/3/4 而不含有表达式 1, 但这样一来, 表达式就不完整了, 类似
console.log(??? + fs.readFile('b.txt', %%))
所以需要用某个东西去替换上面的 ???
. 这个东西便是编译器生成的回调函数的第二个形式参数 (因为 NodeJS API 中许多函数要求的回调都是 function (error, result)
形式, 函数执行结果在第二参数).
综上, Flatscript 编译器在处理上述包含异步函数调用的语句时, 会进行如下转换步骤
- 将表达式 1 以单独语句的形式置于当前上下文中, 除了异步标记其他参数均不变
- 生成一个匿名函数, 其形参是
err, result
, 用这个函数替换表达式 1 中异步标记 - 将上述匿名函数体替换当前上下文, 并在这个函数体中先生成一个分支语句
if (err) 抛出此错误;
- 将上述匿名函数的形参
result
替代表达式 1 放入原语句的语法树中, 而表达式 2/3/4 均不变
经过以上 4 步的变换后, 这条语句
console.log(fs.readFile('a.txt', %%) + fs.readFile('b.txt', %%))
就会变为如下的 JS
fs.readFile('a.txt', // 步骤 1 function (err, result) { // 步骤 2 if (err) throw err; // 步骤 3 console.log( result // 步骤 4 + fs.readFile('b.txt', %%) });
然后继续处理表达式 2, 以及后续的部分, 并为每个生成的回调 result
参数编号, 于是上面的语句最终会被编译为如下的 JS
fs.readFile('a.txt', function (err, resultA) { if (err) throw err; fs.readFile('b.txt', function (err, resultB) { if (err) throw err; console.log(resultA + resultB); }); });
但这样也有一个小问题, 因为语法树的处理在编译时, 以下的语句结构
console.log(syncFunc() + fs.readFile('a.txt', %%))
没有经过特殊处理, 虽然书写顺序上 syncFunc()
调用应该先于 fs.readFile
, 但实际生成的结果会是
fs.readFile('a.txt', function (err, resultA) { if (err) throw err; console.log(syncFunc() + resultA); });
方法选择
在上述代码变换中有一个缺陷就是当一个 %%
标记所对应的回调函数生成之后, 对 err
参数的处理都是直接 throw
, 这当然不对. 在 Flatscript 中引入方法选择在不同的上下文中选择不同的 throw
, return
甚至 break
, continue
语句实际生成的代码.
默认地, throw
语句都直接编译成 JS 的 throw
语句, 但在含有异步调用的 try
语句块内则不然, 此时 catch
对应的语句块会被编译为一个函数, 然后抛出异常的行为就被直接编译为对该函数的调用.
比如, 将前面的例子写入 try-catch 中去
try console.log(fs.readFile('a.txt', %%) + fs.readFile('b.txt', %%))catch console.error($e) # $e 是一个特殊名字, 表示 catch 的异常对象
那么生成的 JS 会类似
function catch0($exception) { console.error($exception);}fs.readFile("a.txt", (function(err, result0) { if (err) return catch0(err); // CALL catch fs.readFile("b.txt", (function(err, result1) { if (err) return catch0(err); // CALL catch console.log(result0 + result1); }));}));
而如果定义形参列表中含有一个 %%
异步标记为的函数 (类似于 ES7 中的 async
函数), 那么在该函数的函数体中所有未捕获的异常, 以及所有的 return
语句的行为都会被替换为回调函数调用. 如
func readTwoFiles(a, b, %%) return fs.readFile(a, %%) + fs.readFile(b, %%)
会生成类似如下代码
function readTwoFiles(a, b, callback) { fs.readFile(a, (function(err, result0) { if (err) return callback(err); // CALL callback fs.readFile(b, (function(err, result1) { if (err) return callback(err); // CALL callback return callback(null, result0 + result1); // CALL callback })); }));}
非正规异步函数调用
以上所有示例中, 都要求定义者或调用者的回调函数形式为 callback(error, result)
(在 Flatscript 中这种形式称之为正规回调). 并且会针对 error
参数生成错误处理语句. 但这样不严谨. 如 JS 内置函数 setTimeout
它并不需要回调参数, 那么生成错误处理函数实属多余. Flatscript 提供了手段类似但生成代码稍有不同的版本. 如
console.log(0)setTimeout(%, 1000) # 使用单个 % 表示这个回调函数不含有 err 参数因此不需要生成错误处理语句console.log(1)setTimeout(%, 1000)console.log(2)
生成的 JS 代码将会类似
console.log(0);setTimeout((function() { console.log(1); setTimeout((function() { console.log(2); }), 1000);}), 1000);
又如 mocha 测试库中, 在异步测试结束后需要调用 done
函数, 这个 done
函数 it
函数的唯一回调参数, 如果将其当作正规回调处理, 那么 done
会被当作错误, 这显然是不对的. 在 Flatscript 中则提供了如下的方式来使用这些不规则的回调参数
describe('test', (): it('async', %done) # 使用 % 加上回调参数名, 若有多个参数, 则如 %(x, y, z) setTimeout(%, 10) assert.ok(true) done())
生成的 JS 代码将会类似
describe('test', function() { it('async', function(done) { setTimeout(function() { assert.ok(true); done(); }, 10); });});
注: 以上示例实际生成的代码都会有一些 name mangling, 以及额外的 try-catch, 为了便于理解和阅读, 在不更改生成机制且不影响执行结果的前提下我手动编辑简化了生成的代码.
其他语言特性
缩进式语法
实现了类似 Python 的缩进式语法结构, 如前面的示例中 try
catch
func
块内的语句的缩进都比这些关键字所在的行的缩进要多.
通常的缩进都比较好理解, 但如果复合表达式中含有一个多行匿名函数, 需要刻意减少缩进以使得多行函数体终止, 比如
setTimeout((): # (parameters): value 是匿名函数定义语法 console.log(0) # 在 (parameters): 后折行, 则后面的行都是这个匿名函数的函数体 console.log(1), 1000) # 直到出现一行内容缩进层次小于函数体中语句的缩进
等价于 JS
setTimeout(function() { console.log(0) console.log(1)}, 1000);
名字检查
Flatscript 是一个强名字检查的语言, 除了如 setTimeout
, isNaN
之类的内建函数或变量之外任何其它名字都必须定义或声明为外部引用. (包括 window
, document
, require
这些名字都需要通过 extern
语句声明为外部引用才能使用)
定义引用要使用如
name: initValue
的语句定义, 名字一旦定义其引用不能修改, 以避免异步上下文中此引用有没有被修改的误解. 子空间内可以覆盖父空间内的名字, 如
x: 10if true x: 20 console.log(x) # 20console.log(x) # 10
但仍然支持修改一个变量的成员, 如
x: {}x.y: 10 # x 现在是 {y: 10}
循环与管道
如果要将任意多个值映射到一组异步的结果, Flatscript 提供以下两种方式 (比如, 从一组文件中读取内容并返回)
func readFiles(files, %%) r: [] for i range files.length r.push(fs.readFile(files[i], %%)) return r
或使用 Flatscript 特色的管道映射操作 (操作符为 |:
)
func readFiles(files, %%) return files |: fs.readFile($, %%)
在管道中使用 $
来引用列表中每个对象的值. (在 Flatscript 标识符不能含有 $
, 函数 $
符号的都是特殊的值, 如前面提到的 catch
块中的 $e
. 如果要使用 jQuery, 需要使用全名如 jQuery('#myId')
)
项目情况
项目地址是 在 OSC 上丢了个镜像
这个项目用 C++11 写成. (其实并没必要, 只是因为历史遗留问题一直用 C++ 写过来了) 需要编译的话请使用
- Linux: g++ 4.8 或以上 (须加
make
参数 COMPILER=g++) / clang++ 3.4 或以上 - Mac: clang++ 3.4 或以上
- Windows: cygwin g++ 4.8 或以上 (须加
make
参数 COMPILER=g++)
有使用 Flatscript 自展的打算.
更详细地可参考 git 上的 wiki 页. 语言详细规范也可以在 wiki 中看到.
另在 上有可直接使用的在线编译 API (编译的代码适用于浏览器因此不预定义 node 中才会包含的变量), 欢迎各种测试.