模仿 ClojureScript 开发的脚本语言 calcit-js

2021-04-05 00:02:16 +08:00
 jiyinyiyong

这个方案大致可以认为是介于 CoffeeScript 和 ClojureScript 之间的一个 js 方言. 简单说, 我是不满 ClojureScript 对于 JVM 的依赖, 想自己试着找个方案绕过, 后面堆功能, 最后实现能用二进制文件运行, 能编译 JavaScript, 算是满足完成自己的需求了. 具备一定的可用性, 比如之前发帖的两个页面, 就是用 calcit-js 做的 .

当然相比 ClojureScript 本身来说, calcit-js 无论功能还有性能, 差距都是很大的. 主要的优势还是脱离了 JVM 跟 Closure Library 这些历史包袱, 容易基于 js 习惯做定制.

项目来源

这几年有 shadow-cljs 以后其实 ClojureScript 的使用体验是不错了的, 然而存在的问题, 也主要是因为 ClojureScript 基于 JVM 开发, 生态方面跟 JavaScript 总有脱节, 对我来说影响大的一些点,

我自己长期用 ClojureScript, 对于一个不可变数据, 支持 macro, 能浏览器运行的语言, 就十分依赖. 所以 calcit-js 对我自己的场景来说, 倒是挺合适的, 而且相关类库也是我自己积攒的. 从开发语言说, 做一个功能减去很多的方言, 并不是什么靠谱的解决方案, 只是试验的程度. 这个项目最初也是试验, 慢慢发现能满足我自己场景的使用, 所以延伸开来.

calcit-js 比起 ClojureScript 更贴近 js 的考虑, 主要是在生成代码方面, ClojureScript 由于项目开发较早, 使用的是 Closure Library 的命名空间的模块化方式, 而不是 CommonJS, 参考 https://developers.google.com/closure/library/docs/introduction 由于当时 JavaScript 没有模块概念, Closure Library 自己发明的命名空间, 比如 goog.math.clamp, 当中 goog.math 是命名空间, 而 goog 是顶级的命名空间, 这套方案跟 CommonJS 跟 ES Import 方案是不同的, 在 dead code elimination 方面做法也不统一.

这样的结果是, ClojureScript 编译出来的代码, 不能直接放到 webpack 跑, 或者说因为 webpack 不识别 goog.math.clamp 这种模块加载方式, 不能打包, 就只能先由 ClojureScript 自己的工具链打包好, 然后跟 webpack 的包连接运行. 加上还有前面提到的 jar 包问题, webpack 也不去读 jar 包的, 两个方案不容易连接到一起. 其他很多编译到 js 的语言, 大家看到都是可以 webpack loader 加载的, ClojureScript 因为这些原因, 就不行.

calcit-js 就在这个功能上做出来调整, 直接编译到 JavaScript 的 ES Import 语法, 这样生成的 js 就可以被 Webpack 打包了, 或者直接当做普通的 js 文件引入, 而 calcit-js 一些核心的代码, 采用 npm 模块的方式维护, 对 JavaScript 用户来说就很自然.

项目的疑点和已知缺陷

calcit-js 跟另一个项目 calcit-editor 耦合严重, 代码存储格式是数据文件的形式, 虽然用的是缩进语法, 但是使用的时候源码文件在单个文件里, 估计挺难习惯的. 我自己开发用的 calcit-editor, 专门的工具了, 也没有 IDE 那种分析了语法提供的辅助. 暂时没有好办法把两块的耦合解开.

Nim 语言问题. 我自己没有 C++ 背景, 所以用的是自己熟悉然后性能尽量快的语言来实现, Nim 首先是编译到 C 运行的, 性能其实还可以. 不过本身也是小众语言. 目前不算大问题, 工具也是完整的. 展示跨平台方面似乎没那么方便, 用的人用偏少. 没想清楚, 后期也许继续这样, 也许考虑下 Rust, 毕竟 Rust 工具更完善.

Persistent Data 实现的性能问题. Clojure 那边用的数据结构精力理论验证优化的, calcit-js 在做的时候当做是试验, 自己粗糙实现了一个 ternary tree, 性能很不稳定. 说到底还是没有设计 tree rebalancing 方案, 数据量真的变大, 性能就没法保证了. 短期自己用没有动力改, 但是也做了模块化, 如果有另一个库, 也能替换上去.

语言测试不够. 自己测试方面经验有限, 只做了简单的测试, 估计细节还是有很多问题.

最终工具链也不完美. shadow-cljs 算是各种整合的方案了, 虽然臃肿, 但是顺畅. calcit-js 自己要热替换, 要同时编译前后端版本的代码, 用 webpack 就很啰嗦了. 这种说到底是工具链整合的事情, 目前 calcit-js 没有做.

一些感想

可能更多还是对于数据结构, 对于 macro, 这两方面的知识的感想吧. 直接使用 JavaScript 的话, 对 data structure 没有那么敏感, 而且内部有优化, 有点玄学, 直接底层语言的话, 不同数据结构性能差异感受就比较直观了, 所以比较能感受到选用正确的数据结构, 对性能提升用多重要.. 这个主要写 js 之前没体会到.

然后是 macro 的使用, 我发现极大简化了这个语言的开发. 作为 Lisp 方言吧, 核心语法寥寥那么几条, 使用当中很多写法就是通过 macro 实现的. 作为 ES6 用户, 语言增加新语法等待浏览器升级, 或者加 babel 插件, 折腾来折腾去的, 有 macro 的情况下, 语法也作为一种抽象方式, 交到开发的人手中, 所以在 calcit-js 开发的时候, 很多从 ClojureScript 继承的写法就用 macro 直接定义了, 这个相比我在 Nim 当中手动一点点把语法码出来, 要省了很多工作量. 特别是在生成 js 过程当中, macro 是自动展开的, 这部分省了大部分工作量. macro 功能来说, 对普通用户可能滥用, 也有性能问题, 但是在语言定义语法的角度说, 很强大.

对我个人比较大的一个影响是, 以前觉得编程语言多么黑科技, 现在看着觉得方案清晰不少, (黑科技更多应该还是在优化方面, 特别是 js 作为脚本语言优化到接近静态类型语言那么快.) 同时这也很大程度因为 calcit-js 是 Lisp 方言, 语言核心就是前缀语法这样简化的版本. 而当这些抽象能力连接的一起, 不那么困难的, 就提供了强大的编程能力, 比如 calcit-js 的 API 文档就是基于 calcit-js 实现的 https://github.com/calcit-lang/calcit-runner-apis/blob/master/compact.cirru

其他

基于小众语言折腾出来的小众语言...

不管怎么说自己终于搞出个自己能用的编程语言了, 算是可以自我满足下. 虽然大量依赖了底层实现, Nim 或者 JavaScript 提供的函数... 另外吧, 虽然语义上是大量参考的 ClojureScript 文档, 语法上却独树一帜, Cirru 风格算是避开了两种主流的缩进语法的套路, 真是自创的玩法了.

相关方向有兴趣的童鞋可以交流下~

如果只是对前端 FP 有兴趣, 建议直接尝试 ClojureScript, PureScript, ReScript 这些. calcit-js 没有类型系统, 确实在 FP 方面挖得很少, 而且主体依然是 js 用户的习惯.

ClojureScript 本身对 ES Import 的支持, 也不是没有尝试, 主要是我觉着太累了.

1664 次点击
所在节点    编程
1 条回复
jiyinyiyong
2021-04-30 00:35:32 +08:00
换成用 Rust 重构了 https://github.com/calcit-lang/calcit_runner.rs

Rust 大法好~ (over Nim

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/768033

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX