This repository has been archived by the owner on Jun 13, 2021. It is now read-only.
calcit-runner 的初步使用介绍 #220
tiye
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
整体上说, Calcit 的语义是基于 Clojure 再衍伸的.
由于我早先的代码是编译到 ClojureScript 运行的, 我就需要对基础功能做兼容.
为了实现语言的方便, 具体代码会根据情况有调整, 以及重命名.
除开语义, 跟主要的编程语言的不同在于代码的存储格式.
Calcit 使用的是 calcit-runner 生成的 snapshot 文件, 是单个文件, 所以很不一样.
具体内容下面分开解释.
作为参考想要直接查看语法的话, 一些现成的代码可以浏览:
Calcit 数据格式
calcit-editor 是一个属性编辑器, 沿用了 Clojure 的命名空间,
最后保存 snapshot 的时候, 所有 namespace 也就继续存储在一个文件当中了,
主要代码部分保存在
:files
这块数据当中, 下面的例子只写了一个 namespace:除了
:files
, 数据还有:configs
保存着代码运行相关的配置,其中
:init-fn
是程序运行的入口,:reload-fn
是热替换后触发的函数,无论是 Nim 模式的代码的执行, 还是 js 模式 vite 进行热替换,
:reload-fn
都尽量被使用.基于 Clojure 语义的功能, 以及不同
功能上讲, Calcit 实现的功能非常有限, 只是一个用于前端的脚本语言,
从 ClojureScript 继承过来的功能包括:
nil
:keyword
'symbol
|string
true
false
123
recur
的尾递归:as
:refer
这也只能笼统地说从 ClojureScript 学习了一些概念,
Clojure 更多的更加精髓的内容, 并没有复制过来.
而且实际使用很容易碰到跟 Clojure API 对应不上的情况, 所以还是不能主观就认为是兼容 Clojure.
从不同来说, 首先 String 的写法就不一样, Calcit 用的是 Cirru 的风格,
|a
"|a b"
表示"a"
和"a b"
, 引号的作用是特殊字符做识别,而
|
是作为前缀语法存在的, 同时"
等同|
作为 String 的前缀语法,"a
"\"a b"
,使用
"
主要是在 calcit-editor 当中, 更容易识别.数据结构方面,
List
对应 Clojure 的 Vector, 支持随机访问,Clojure 当中由于
Sequence
和Vector
的区分, 以及默认 Lazy, 会形成一些困扰,比如
(map inc [1 2 3])
返回的其实是(list 2 3 4)
, 而且是 Lazy 的结构,Calcit 当中按照 JavaScript 的习惯直接用的是 eager evaluation 的
List
.也做了一些优化, 只是最终效果不稳定, 特殊情况会比 Clojure 的 Vector 慢很多.
不过这个好处也就是数据结构统一了, 数据转化方式更直观一些.
另外
case-default
&doseq
let-sugar
的用法, 跟 Clojure 也存在差别,具体都要看文档 http://apis.calcit-lang.org/
函数和尾递归
这边主要列举一些用法了, 比如定义函数,
函数会返回最后一个表达式的数据, 即便最后的表达式是个注释, 那么返回值成了
nil
.函数参数中可以通过
&
来标记 arguments spreading 语法,以及调用函数时候, 也有一个类似 JavaScript 语法的 spreading 行为:
另外对应 TypeScript 的 optional argument, 有一个
?
的语法,其中
b
可以不传, 默认也就是nil
.如果不用
?
语法, 由于 Calcit 会进行参数个数的检测, 那么可能会报错.尾递归类似 Clojure, 采用
recur
进行手动标记, 这样能简化语言的实现,函数的简写跟 Clojure 不同, 用的是
\
以及衍生的\.
,其中
\
默认最多带两个参数. 如果参数多的话, 还是定义参数名称为好:至于
\.
其实目前看来未必实用, 因为是一个柯理化版本:数据结构
List 上边讲过了, 接口是类似 Clojure 风格的:
List 内部用了两种优化,
Array
和TernaryTreeList
, 后者是 3 branches trie 的结构.Array 性能好, 但是操作的时候消耗内存大,
TernaryTreeList
相反,至于 Clojure Vector, 用的是 Hash Array Mapped Trie, 两方面都比较稳定,
Calcit List 的好处是使用方式统一, 调试清晰, 部分情况 Array 性能更好..
当然, 总体讲是不如 Clojure Vector 的, 数据结构的优化做得不够深.
Map 内部用的是
TernaryTreeMap
, 也是性能不稳定, 差的时候会非常差.Set 基于 host language 做的, 使用场景较少, 没有专门做优化, 一般只用于处理 literal,
至于 Record, 跟 Clojure Record 的区别就比较大了,
应该说是对于固定了 fields 的 Map 做的特化的版本, 内部实现也不同, 性能就比较稳定了,
具体的设计可以看 #204
控制结构
首先是直接合并操作的
do
:批量操作有个
&doseq
, 参数写法只支持一个,if
表达式:case
表达式带着一些困惑:cond
用true
代替了:else
的用法,然后 thread macros 也是搬过来的:
状态
虽然用了 Atom 的概念, 但是 Calcit 里边只有顶层定义才能用
defatom
,实际的行为是这个 Atom 默认就用了类似 Clojure
defonce
的方式在 namespace 上 attach 了,Atom 设计上是在热替换过程当中保持数据, 而不会进行重新初始化的, 有点特别,
至于操作上, 还是用的 ClojureScript 带来的习惯:
JavaScript Interop
参考 ClojureScript 有几个基础的 interop 语法,
模块应用当中, 参考 ClojureScript 可以使用字符串标记 js 模块, 包括
"./a.js"
这样的相对路径.极端的一些模块, 可能需要
ns/@
或者ns/default
这样的特殊语法来处理,其中
ns/@
应对用了modules.exports = function(){ ... }
的模块, 这是 ES Import 当中不合适用的.Calcit 也只能算是基本实现了跟 js 的互操作,
因为没有 ClojureScript 的历史包袱, 一些脏的方案是可以比较容易加上的,
整体说, 其实还缺很多, 比如
async/await
语法目前就没有, 而实际上很容易用到.由于没有规划, 后续也不确定哪些功能会在什么时候加上.
另外注意 calcit-js 的 runtime 是以
@calcit/procs
这个 npm 包形式存在的,运行还有打包的时候, 这个依赖也少不了.
这些文件目前还是以
*.js
的文件后缀存在, 对*.mjs
的迁移还要看后续的计划.Macros
大体上还是参考的 Clojure 的写法, 用了
defmacro
和~
~@
,不过 Cirru 语法当中没有 syntax quote, 就只能专门定义一个操作来表示 syntax quote 了,
由于我不适应
quasiquote
这个奇怪的单词, 直接简化成了quote-replace
:由于内部实现的特殊性,
~a
在 token reading 过程中实际上是~ a
这个表达式,所以内部其实会用到表达式版本的写法,
Calcit 内部在 symbol 上设置 resolved 字段保存了 namespace 信息, 对应 hygienic macro 的处理,
由于没有找对准确的参考, 怀疑还是存在一些 edge cases 没有处理到.
调试 macro 一直是很麻烦的事情,
macroexpand
是有实现的.此外程序报错时会生成一个
.calcit-error.cirru
文件, 其中包含调用栈信息,其中的代码是展开后的表达式, 对调试 macro 有一定的帮助,
我自己也搭了简单的 error-viewer 页面用于查看这个报错.
IR 导出
目前 Calcit 命令行有几个常用的用法
这时还有一个
--emit-ir
的参数, 可以生成一个js-output/program-ir.json
文件# 用于生成 JSON 格式的 IR 文件 cr --once --emit-ir
这个文件当中包含程序预处理, macro 展开以后, 程序的整个信息.
生成 js 用的就是这份信息. 其中包含预处理以后符号的引用信息,
文件比较大, 需要用 ir-viewer 展开查看.
特殊的情况, 可以用来定位 macro 展开以后, 变量引用是否正确.
这份信息能够用于生成 js, 也就意味着可以用于生成其他的代码后端, 理论上.
接口稳定性
Clojure 作为起点, 短期不会有大的调整.
如果有调整, 考虑到已有的项目, 还是会尝试兼容的方式去调整的.
比如参数顺序, Clojure 中
(assoc {:a 1} k v)
(map inc [1 2 3 4])
一个在前一个在后,在 Haskell 中, 作为 operand 的参数都是放在最后的,
而在 OOP 风格的语言当中, 作为操作对象的参数又是放在最前的,
这就有不少的不一致性, 当前的实现为了兼容采用的是 Clojure 的写法,
但是这种不统一确实也是作为一种思维负担, 而且我也没明确的思路能统一好.
从实用角度来说, 整体统一到第一个参数, 能够符合更多人的使用习惯. 没想好.
其他
目前 Calcit 整体项目处于自娱自乐的状态, 包括类库也是我自己以前在 ClojureScript 环境用的那些.
由于 calcit-editor snapshot 这个格式的限制, 别人其实更难尝试用.
早期版本的 CirruScript 至少还是跟 CoffeeScript 那样单个文件 transpile 用的, 还能局部用,
calcit-js 这个, 就是从编辑器到运行环境整体的工具了, 没法拆开使用.
后续计划主要还是看我自己使用的进度.
相比 ClojureScript 诸多限制, calcit-js 生成的算是比较正常的 ES modules 代码了,
跟 vite 跟 webpack 配合使用问题不大, 缺失的很多东西还是可以补上的.
Beta Was this translation helpful? Give feedback.
All reactions