Cloel: 用Clojure扩展Emacs

其实外部语言, 主要还是看外部语言的生态, 语言之前风格切换都是可以克服的。

比如Python, 一旦互调用起来, 基本不用管生态的问题, 啥库都有。

NeoVim 那些人对接了Lua后, 其实也发展很好, Lua的生态还不如 Python, 主打一个执行速度快。

1 个赞

感觉Clojure很不活跃,用户太少了,案例太少了。

我简单的目测了一下 clomacs 的源代码, 我觉得 clomacs 那样实现的性能是很有问题的。

colmacs 虽然有 async 的 callback, 但是我觉得它的思想还是启动一个 nrepl 的 server, 然后一条一条的执行 Emacs 发过来的 Clojure 函数代码, 这样不管 colmacs 的 sync 还是 async 设计, 本质 nrepl 是 block 的, 一旦某一个 Clojure 的计算量大了以后, 虽然不卡Emacs, 但是结果半天也返回不了, 甚至是多条 Clojure 计算任务, 只要中间某一个 Clojure 函数太耗时, 其他任务也会卡住。

可以认为 Colmacs 的设计现在是一个外部进程的 node.js 协程设计, 虽然不卡Emacs, 但是它自己一条一条的执行会卡住。

我的设想是: 开发调试时, 依然用 Cider 去一条一条的测试, 就像我们有时候在终端用 ipython 测试 Python 想法一样

但是真正的扩展框架应该像 lsp-bridge 那样去设计:

  1. 首先把 Clojure 隔离到外部进程
  2. Clojure 外部扩展框架是一个独立的程序, 不要和 Cider 以及 nrepl 有关联, 这样太复杂了
  3. Clojure 外部进程事先启动起来, 避免 Clojure 启动太慢, 我发现比 Python 启动还慢, 哈哈哈哈
  4. Emacs 和 Clojure 外部进程用 JSON (或者 Sexp S表达式)来通讯, 所有编程都是异步的, 不允许用同步方法调用
  5. Clojure 接受到Emacs发送的任务, 全部放到子线程里面去执行(这是和 Colmacs 最大的区别), 这样外部进程就可以像 lsp-bridge 那样并发的接受很多请求, 而不会因为任何一个Clojure函数卡住(每个函数都在各自的线程中独立运行)

这样设计应该可以实现和 lsp-bridge 一模一样的性能, 同时让外部扩展程序也是 Lisp 风格。

3 个赞

猫大这是想引入JVM生态吗?

是呀,我觉得啥语言都试一下,这样emacs慢慢地就又快又有库生态了

2 个赞

引入一个lisp语言扩展想法挺好,贡献者会多不少。只是JVM的话有点太笨重了,可能比node还不受待见 :joy:

3 个赞

真的笨重,用过cider就知道起一个repl要眼睁睁等上老半天

1 个赞

能先启动外部进程,不用的用户,做再多他都不会用。

1 个赞

也不是 clojure 启动慢,大部分 Java 应用启动都慢

我有点儿好奇,使用 clojure 真的能加速 Emacs 吗🤔 在 Emacs 中 Elisp 总是写得最称心的语言,我有点儿怀疑并没有那么多愿意使用 Clojure 扩展 Emacs 的开发者,也很难建立起像 python-bridge 那样的社区。

这个不必担心吧。就好像 C 火起来是因为 unix,python 火起来是机器学习,只要有一个足够强大用的人足够多的产品,自然有人来为这个环境买单。

有多线程的语言都可以加速,只是编写体感的差别。

clojure 的话,可以使用 Java 的 virtual thread,类似 goroutine 的东西,每个任务一个线程呢。

不过与其做成任务,是不是做成连线会更好?主线程接受配置请求设置全局配置,接受连线请求构建一个新的子线程再与客户端连线,客户端收到子线程的 id 之后都与子线程沟通。这样一个 server 可以同时启动多个分离的连接,而每个客户端也可以有多个插件,每个插件都好像和单独的 clojure 实例沟通一样。

当然这样实现比较复杂可能涉及协议,可能不如服务端不保留状态的实现起来简单。

另外 clojure 和 emacs 的沟通格式,都不妨设置一种转接器,内部都是用顺序表和散列表,沟通中使用 Json 还是 S Expr 甚至 XML 都可以,转接器能把中间状态转回内部顺序表和散列表就好了。

设计一套类似于LSP的协议?然后就语言无关了。这套内部机制最好用C或者rust来搞,性能和扩展性都有了 :grinning:

非也,只是 clojure 和 emacs 间沟通的,与其说是 LSP 更像是 tcp。建立连接后这套协议就不管事了,客户端和服务端到底发送什么消息往来他们自己决定。

感觉还是缺少一些更加实际一点的应用例子?

外部进程完全可以解决启动慢这个问题, 比如在 Emacs 启动的时候去判断是否有一个 lisp server 在运行, 若无则再新建一个 (简单用的启动代码):

;; emacs lisp
(condition-case nil
    (sly-connect "localhost" 4005)
  (error
   (message "SLY not connected. ")
   (when (string= "y"
		  (completing-read "Create a Lisp backend? [y/n]: " '("y" "n")
				   nil t "y"))
     (shell-command
      (concat "screen -dmS sbcl " ;; 用 screen 让 SBCL 在后台运行
	      "sbcl --dynamic-space-size 2048 " ;; 这里完全可以用 dump image 来加速启动
	      "--eval \"(ql:quickload :slynk)\" "
	      "--eval \"(slynk:create-server :dont-close t :port 4005)\""))
     (run-with-idle-timer
      1 nil #'(lambda () (sly-connect "localhost" 4005))))))

(Clojure 没怎么用过, 我记得应该 nrepl 的实现里面也有连接到现有的 Clojure 环境中的操作)

协议的话, 有一个 swank-js (给 JavaScript 一个 SLIME 的接口, 用来和 Emacs 通信).

或者也可以参考: SLY Slynk RPC Protocol?

1 个赞

是的,编程框架和LSP不一样。

LSP有特定功能,编程框架更像是TCP链接,借助外部语言的多线程能力。

clojure的最佳编辑器是emacs。有共同的群众基础。 :grinning:

1 个赞

我倒觉得可以扩展下思路