Calcit 一些基础介绍 #79
tiye
started this conversation in
Show and tell
Replies: 1 comment
-
Some examples:
println 1
println true false
println "|this is a string"
println :keyword-a
println $ [] 1 2 3 4
println $ {}
:a 10
:b $ [] 20
:c $ {}
:d true
println $ #{} :a :b :c
let
Demo $ defrecord Demo :name :data
println "|special structure a record"
%{} Demo
:name |demo
:data 1
println $ [] 1 2 3 4
println $ range 100
println $ foldl (range 20) 0 &+
println $ append (range 10) 11
println $ slice (range 10) 4 6
-> (range 10)
map $ fn (x) $ * x x
foldl 0 &+
println
->
{}
:a 1
:b 20
map-kv $ fn (k v)
[] v k
println |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
为什么要有 Calcit
Calcit 是我自己拼凑出来的一套脚本语言, 功能参考 Clojure.
开发的初衷是解决 ClojureScript 对 JavaScript 生态支持不佳的问题.
在开发过程当中, 也将我在 Cirru 项目中的玩法引入进来, 调整了编辑器和源码.
所以 Calcit 也可以认为是两个意图的融合体.
那个 Clojure 是好在哪? ClojureScript 对 js 是不友好在哪?
我个人受到 React 影响, 受到 Haskell 影响, 对函数式编程有不少认同,
Clojure(Script) 通过语言内实现 Persistent Data Structure 来支持函数式编程,
一方面我们需要函数式编程那种"隔离副作用"的约束, 来凸显"单向数据流",
一方面我们语言层面的"不可变数据", 让代码更贴近数学和逻辑的语义,
同时, 我们也需要保证 FP 编程方式有恰当的性能, 在数据结构层面进行优化.
而 Clojure 的 Persistent Data 的道路, 已经被很多新发明的编程语言认可.
我觉得目前来说这算是比较巧妙的一个平衡.
不过需要注意的时候, Calcit-js 版本中使用的 three finger tree 性能优化不够好,
所以目前实现是配合一些简单的 Array 实现做简单场景的优化的. 勉强不影响使用.
ClojureScript 的问题在于, 它选择了 Google Closure Compiler 作为底层,
因而也就使用了
goog.x.y.z
这样的 namespace 作为模块组织方式,而这跟当前前端社区 Webpack 甚至 Vite 走向的 ES Modules 并不能相配合.
对于具体的工程实践来说会是带来很多琐碎并且烦人的问题.
在我使用 ClojureScript 的五年当中, 反复出现此类问题.
虽然在 shadow-cljs 的条件下大幅简化了用户的麻烦, 但是方案也越来越重.
shadow-cljs 运行在 JVM 上, 同时又依赖 Babel 来转义代码, 做了过多工作.
在 ES Modules 逐渐得到浏览器支持的当下, 逐渐变得多余而臃肿.
随之而来的也有 JVM 启动性能的问题, JVM Clojure 启动慢, 也占用内存,
这也导致 shadow-cljs 在低配置的机器或者 CI 环境不够灵活.
而对于小型的项目而言, 一个快速启动快速切换的方案, 有非常大的价值.
本来我想的是试验一个简单的脚本语言, 先自己尝试用, 所以开始尝试.
最终发现多个想法汇集到一起, 经过一段时间努力, 我在自己的项目中大量用上了 calcit-js.
https://github.com/calcit-lang/calcit_runner.rs/network/dependents?package_id=UGFja2FnZS0xODMwNjYyMjk0
从另外的角度讲, calcit-js 是可以在大家未探索的中间地带补上一些碎片,
在 ClojureScript Persistent Data 和 JavaScript 现今的 ES Modules 之间,
在文本格式的代码, 和 Graphical Code Editor 之间, 做长期的试验.
对我自己来讲, calcit-js 是 Respo 框架的基础, 也是其他一些微型框架的基础.
安装
当前版本的 Calcit 是基于 Rust 实现的, 我没有提供多平台的 binary 文件,
所以, 就需要基于 Rust 的源码, 编译安装, 源码在:
https://github.com/calcit-lang/calcit_runner.rs
目前语言非常小, 所以编译还是非常快的, 十几秒大概?
cargo install --path .
安装完成, 会有两个命令
cr
和bundle_calcit
,cr
就是语言主体的编译命令了, 执行脚本, 或者生成 JavaScript 代码,而另一个是, 如果你不用 calcit-editor, 而用文本缩进语法, 就需要用到.
cr
命令可以动态执行, 不过目前没有 REPL, 只能从 eval 入口进入,Calcit 里表示 String 的语法很奇怪, 需要点时间适应一下.
其他一些常见的功能, 可以在 eval 模式继续尝试.
习惯上 calcit-js 指代编译到的 js 运行的这部分语义, 对应
cr --emit-js
,生成的 js 代码, 现在是用的
import
export
语法, 但是用的*.js
后缀,你可以认为这是为了配合 Vite 用的, 因为 Vite 刚好能这样子运行,
理想化地说, 整个该统一往
*.mjs
迁移过去, 统一上 ES Modules 语法,但是目前 calcit-js 需要考虑 Node.js 环境热替换的问题, 挺无奈的.
Node.js 生态直接使用 mjs 障碍多多, 社区还在迁移过程当中,
而且 Node.js 热替换远没有 Vite 这样成熟的方案, 现在还是依赖 Webpack.
需要看细节, 目前只能查看我某些项目中的配置, 再跟我讨论细节了:
https://github.com/Cumulo/cumulo-workflow.calcit
基本语言特性
Calcit 基础了 Clojure 的一些基本数据类型以及 API
Calcit 从 Lisp 继承的是 Macro 的功能, 元编程,
因而模仿 Clojure 将不少的内部语法以 Macro 的方式提供.
包括引入了 Clojure 的
->
作为 pipeline 的组合方式.而 Macro 也确实提升了语言的表达能力.
在 eval 模式, 配合 Bash 或者 Zsh 的多行参数, 可以做一些尝试.
(注意
=>>
是我的命令提示符... 你的可能是$
)前面提到 Calcit 表示 String 语法比较奇怪, 这是从 Cirru 项目继承过来的,
需要前缀的
|
或者"
来表示, 但是引号也用于转义, 所以代码就很奇怪了,注意平时引号是省略的, 当有特殊字符引号是必须加上的,
(这个限制是从树形编辑器那边带过来的, 所以你会觉得反直觉)
这边得解释一下
$
的用法, 借鉴自 Haskell,$
用来减少()
的使用, 表示后面的代码是一个表达式整体,下面这样的写法是等效的:
由于 Cirru 当中缩进也是设计来替换
()
的,所以下边的写法也是等效的:
总之 Calcit 当中使用的 Cirru 文本语法是非常灵活的一套语法.
Calcit 的
->
借鉴自 Clojure, 但是也受到 Nim 语言不小的影响,Clojure 跟 Haskell 习惯性把 List 放在参数末尾, 但是 Calcit 习惯放在开头,
放在开头是因为想借鉴面向对象, 或者说为了动态地实现 Polymorphism.
这边使用
do
是因为命令行会误识别-
, 代码内部不需要.或者直接换行再写:
Calcit 有跟 Clojure 类似的但稍作调整的控制结构,
注意
let
直接用的话需要多重的缩进结构,这里的
let
是 Macro, 内部是&let
, 也有其他一些变种:然后我们有条件语句:
函数尾递归目前是通过手动标记来实现的, 需要解释器支持,
编译 calcit-js 的时候, 会生成一个循环, 但其实也有一些额外的开销.
关于大致的 API 介绍, 你可以在 API 文档当中查找,
http://apis.calcit-lang.org/
不过目前的文档, 确实可以用苍白来形容. 慢慢加了.
更多的, 你还是要先有 Clojure 的一些基础, 然后再来看 Calcit 是怎样用的.
...以及 Calcit 相对 Clojure 去掉了哪些功能, 做了哪些简化.
抽象能力
首先对照 Clojure, 当你要表示数据的时候, 可能就是 HashMap,
其中用
:type
字段动态得标记当前数据的类型,这是一种比较原始比较常见, 但是传输比较友好, JSON 都常用的抽象方式,
大致对应的 parametric polymorphism 吧, 不过在动态语言里就啰嗦了.
在 Rust 等语言, 通过 Pattern matching 可以把代码写得很精简,
这在 Calcit 只能用 macro 模拟, 我尝试下来感觉也不好, 也不类型安全.
同时我跟 Haskell 跟 Rust 对照, 感觉到在 ad-hoc 多态方面有缺陷,
这方面比较深, 你对照 Rust 和 Clojure 的 Expression Problem 再看 Calcit 是能感受到的,
http://cswiercz.info/programming/2018/10/30/the-expression-problem-in-rust.html
https://eli.thegreenplace.net/2016/the-expression-problem-and-its-solutions/
显然 Clojure 设计 Protocol 设计 multimethod 是为了解决这类问题,
而这在 Calcit 里并没有得到解决, 我经过思考, 只能加了一个相对啰嗦的语法,
这部分细节有留下一些说明, 不展开了: #44
总之我对 Calcit 进行了一下改造, 包括内置的数据类型, 也做了处理,
所以你会看到很多 Method 语法的使用场景,
以及后边又衍生出来通过 virtual table 动态实现的 method 调用来模拟多态,
这个例子中
:: Num 2
就是用 Tuple 来强行模拟 Tagged Union 实现多态,我也不深入解释了, 总之整个语言经过这次改造, 也算支持一些多态了,
不足之处是这个修改对性能有一些影响, 包括在 calcit-js 那边也有一些影响.
好处是通过借鉴 Rust 的多态, 语言层面上暴露的接口更清晰了.
那么有了多态, 加上 Lisp 本身作为 DSL 和 FP 语言的灵活能力,
使用 Calcit 进行编程, 就有比较强的抽象能力了.
尽管还是不能和支持 Type class 或者 Traits 的语言比, 也没有类型安全... 勉强够用吧
不实用的原因
虽然我自己已经使用了 calcit-js 几个月, 修复了一些常见的 bug,
但是我也清楚地了解到它还远远不适合作为一门生产环境使用的编程语言.
现在有如下的一些问题:
模块管理, 版本管理缺失
calcit-js 目前从
~/.configs/calcit/modules
目录下加载依赖,读取模块时按照一个路径的字符串读取合并的一个源码文件, 注意是"单个合并的文件",
也就是说如果要做依赖管理, 在路径上做文章即可, 方案是开放的,
但同时, 没有工具帮忙管理, 也没有多版本共存的支持,
Calcit 当中使用的是 namespace 组织模块, 当然是无法多版本共存的.
目前就像早期的 Golang 类似, 通过 GitHub 仓库维护模块,
而更糟糕的是, 没有提供命令行工具用于抓取依赖, 依赖手工脚本.
这样一个简陋的现状, 对于大规模生产来说明显是不够的.
在 CI 环境通过脚本还是可以维护的, 甚至配合 GitHub Tags 控制版本,
https://github.com/calcit-lang/editor/blob/main/.github/workflows/upload.yaml#L26
不过, 这也还是没有描述递归的依赖, 同时隐式地要求尽可能用最新版本.
生产环境的项目需要强大的版本管理.
编辑器工具不完善
或者说 Calcit 依赖的 Cirru, 在试验的道路上走得太远了一些.
Lisp 当中有 "Code is data" 的说法, 而 Calcit 更进了一步,
Calcit 的源码是以"数据"的格式存储的, 得益于 Cirru 的形态, 还算自然,
(编辑器原始数据) https://github.com/calcit-lang/editor/blob/main/calcit.cirru
(代码数据) https://github.com/calcit-lang/editor/blob/main/compact.cirru
而当前我使用 Calcit 的方式, 也是用树形编辑器, 而不是纯文本的编辑器,
不论好坏, 这对于新人上手都是不小挑战, 意味着极高的培训成本.
就算抛开树形编辑器, 直接文本的方式写代码吧, calcit-js 其实是支持的,
但其中使用的缩进语法, 还有额外的打包的步骤, 对热替换方案的破坏...
你可以认为是回到了 CoffeeScript 早期的情况..
缩进, 没有 SourceMap, 没有热替换, 放到今天, 是很拉低开发效率的.
我认为这不是设计的问题, 完全是工具缺失的问题. 短期也解决不掉.
我自己使用的树形编辑器, 也就是说我自己的场景, 我还是可以保证效率的.
这也是我还希望给人推荐 Calcit 的原因.
树形编辑器, 结构化编辑器
目前我自己使用的是定制版本的 Calcit Editor 来开发,
https://github.com/calcit-lang/editor
保存的时候是数据文件, 编辑的时候是树形的结构, 能做结构化编辑.
没有类型系统, 但是基于结构化编辑能比较快地跳转, 能保证一定的效率.
限于文章篇幅, 不打算深入讲了, 有兴趣可以搜索我以前关于 Cirru 的文章.
需要解释的部分主要是关于热替换, Calcit Editor 针对这部分做了一些适配,
当代码发生更改的时候, 编辑器会额外写一个
.calcit-inc.cirru
的文件,这是一个数据文件, 其中记录了更改的那个定义以及新增部分的代码,
cr
命令行, 或者说解释器内部, 是以这份数据作为增量的工具的,在 Rust 环境运行时, 就是用这部分数据替换 runtime, 然后再执行 reload 对应函数.
而在生成 js 时, 由于 macro 的影响, 还是会比较多地重新初始化 runtime, 再生成代码.
也就是说, 如果不用 Calcit Editor 导出增量信息, 就只能做全量的更新,
虽然性能也不太差, 但是目前需要额外的 watch 之类的方案, 比较麻烦.
我之前有一个演示, 就是用
bundle_calcit
配合缩进语法写代码,https://www.bilibili.com/video/BV1ry4y1W7VW/
而这个方案当中, 就需要你自己去 watch 文件, 然后重新执行 bundle_calcit 了.
前面说的编辑器生成的文件对应的热替换, 主要是对应到 Rust runtime 的行为,
具体到 JavaScript 代码的热替换, 现在主要依赖 Vite 和 Webpack 完成,
这部分处理, 目前有成熟的方案, 但是封装做的不好, 具体留了一些视频介绍.
https://www.bilibili.com/video/BV1of4y1s7ER/
https://www.bilibili.com/video/BV1uq4y1W7E4/
其他
目前 Calcit 相关的文档只有 API 文档 还算有维护, 总体缺少文档,
这篇文章也主要是补充一个介绍性质的内容, 其他方面后续还要补充.
近期的一些进展有留视频介绍, 不过内容较长, 估计没什么人爱看了.
https://space.bilibili.com/14227306/channel/detail?cid=185146&ctype=0
Beta Was this translation helpful? Give feedback.
All reactions