Clojure開発環境 Elin を作った話
この記事は Clojure Advent Calendar 2024 の9日目に向けたものです。
TL;DR
-
vim-iced の後継のClojure開発環境としてElinを作ったよ
- 使い方はこの記事では説明しないので、それはドキュメントを参照してね
-
Babashka上で動くサーバーによって動作するので、機能のほとんどはClojureで書かれているよ
- Elin自体もElinを使ってREPL駆動開発しているよ
Elin とは
Elinはvim-icedの後継となるClojure開発環境として作ったものです。
vim-icedはVim scriptで書かれたVim/Neovim向けのClojure開発環境であり、Vim/Neovim両対応したものの中で高機能な方かと思います。私自身日毎の業務でClojureを使った開発をしているので、業務レベルで問題ない開発効率を提供できるだけの機能は提供できていたと自負しています。
ただvim-icedにはいくつか課題がありました。
- Vim scriptが遅い
- nREPLと通信する際のbencodeのデコードが遅く、処理によってはVimの操作がブロックされることがあった
- Vim scriptに型がない
- 機能を追加すればコードベースは自然と大きくなり、それにつれて型があれば気付けるようなバグが増えてきた
- テストがしづらい
- 書き方の問題もあるがVim scriptには標準のテスティングフレームワークがなく、nREPLサーバーと通信する処理などはきれいにテストが書けず、壊れやすいテストになってしまっていた
- そして壊れると問題の箇所が把握しづらく運用が難しかった
これらを解決するためにVim scriptよりも速く動き、型チェックができ、テストもしやすい構成を目指して作ったものがElinです。
Babashka
Vim scriptよりも速く動き、型チェックができ、テストもしやすい構成というとVim界隈ではDenopsが有名です。DenopsではTypeScriptを使ってVimプラグインを書くことができ、処理はDenoで動くために非常に高速です。型は勿論、テストなど諸々はdeno
コマンドでほとんどのことができて困ることはありません。
しかしElinでは処理のほとんどをBabashkaを使いClojureで書くことにしました。
最初はDenopsが最適だろうと思い実際に簡単な評価や定義ジャンプ、補完などができるプロトタイプまで作りはしました。2021/07頃から始めて隙間時間で少しずつ何度も作り直しをしたりして2023/01頃にようやく形になったのですが、その経験からTypeScriptで書かれたClojure開発環境を何年も運用するだけのモチベーションは保っていられないなと遅ればせながら思い至りました。
自分のOSSなので自分が楽しく運用できるようにしたい、Clojureの開発環境はやっぱりClojureで書きたいという気持ちが強くなってしまい、それであればDenopsがDenoでやっていることをElinではBabashkaで再現してみよう(!)と考えて今に至ります。(ただしDenopsとは異なり、フレームワークとして他プラグインの動作をサポートするということはしておらずElin専用の仕組みです)
なお静的な型チェックはClojure自体にはないので、Elinではmalliを採用しています。
malliについては以前malliとclj-kondoで変わる(?)Clojure開発体験という記事を書いているので興味があればこちらもご参照ください。
仕組み
Elinはエディタ(Vim/Neovim)がBabashkaで動くサーバー(Elinサーバー)と連携することで動作しています。
サーバーのプロセスはエディタ起動時に裏で立ち上げられて、エディタから自動的にサーバーに接続されるようにしています。エディタ・サーバー間の連携は具体的にはVimではJSON
で、NeovimではMessagePack
で通信して連携しています。
エディタとElinサーバーとのやりとりはClojureのProtocolで定義されたインターフェースを通じて行うようになっているので、理論上はVim/Neovim以外のエディタでもElinサーバーとの通信部分さえ実装できれば動作する構成となっています。(ただこれは主の目的ではなくVim/Neovim両対応するための副産物です)
Elinサーバー
前述の通りElinサーバーはBabashkaで動いています。
Elinサーバーは以下3つの役割の保証に専念していて、追加の機能はハンドラー、インターセプターで提供する方針です。(機能的には他にも提供しているものはあるが主の役割はこれだけ)
- nREPLサーバーとの接続の管理
- エディタからのリクエストの管理 (Handler)
- 様々な操作の挙動を変更する仕組みの管理 (Interceptor)
Handler
ハンドラーはエディタからのリクエストを処理するためのものです。
例えばVim/Neovim側で :ElinEval (+ 1 2 3)
というコマンドを実行する際elin.handler.evaluate/evaluateというハンドラーがリクエストされます。
Elinサーバーではこのリクエストに従いハンドラーの処理を実行し、必要に応じて結果をエディタ側に返します。
command! -nargs=1 ElinEval call elin#notify('elin.handler.evaluate/evaluate', [<q-args>])
よってハンドラーは主に新しい機能を提供するために使われます。
エディタ側はコマンドの定義とハンドラーのリクエストを送るだけとし、実際の処理はElinサーバー側のClojureで実装できるようにすることで速度の確保とclojure.testでテストしやすい構成を実現しています。実際ハンドラーは単なる関数なのでリクエストさえモック化できればテストは簡単に書けます。(e.g. nREPLサーバーとの接続処理のテスト)
Interceptor
インターセプターはnREPLサーバーとの通信やハンドラーでの処理などを途中で変更するためのものです。
例えば (comment (+ 1 2 3))
といったコードを ElinEvalCurrentTopList
コマンドで評価する際、elin.interceptor.evaluate/unwrap-comment-formというインターセプターを経由することで ElinEvalCurrentTopList
コマンドが実際に評価するコードは (+ 1 2 3)
とすることができます。
よってインターセプターは主に既存機能を拡張するために使われます。
vim-icedではHookという機能を提供して既存機能を拡張できるようにしていましたが、Hookは後付けであるためにHook出来る場所が限られていたり、Hookで実現できることも機能毎に異なっていました。また処理の前後でHookしたい場合はそれぞれ別のHookとして定義する必要もあり機能の統一性に欠けていました。
このHookに関してはDenopsでのプロトタイプの頃からInterceptorという概念で置き換えることは決めており、Elinでもその方針は同様です。
コアの役割としてインターセプターを持たせることで、コアの機能を小さく保ったまま拡張できるようになっているのが Elin の推しポイントの1つです。
実際の拡張の例を2つ挙げます。
- デバッグ機能
Elinはvim-iced同様にcider-nreplの力を借りてデバッグ機能を提供しています。
これはnREPLサーバーとの通信の中で評価途中に急遽デバッグに関する通信が挟まるような挙動のためvim-icedではnREPLサーバーとの通信処理とデバッグ処理が密結合していました。
しかしElinではnREPLサーバーとの通信もインターセプトできるため、デバッグ機能はインターセプターとして実装できています。
利用しやすさのためにElin本体に組み込まれてはいますがインターセプターとして実装されているということは外部プラグインとして切り出せるということです。更に言えば外部プラグインとしてより高機能なデバッグ処理を提供することも可能ということです。
- Docstringの参照
Clojureで開発をする中で関数の使い方を知るためにDocstringなどを参照することは多いと思います。Elinでも勿論 :ElinLookup というコマンドを提供していますが、これもインターセプトできます。
Elinでは型定義としてmalliを採用しているというのは前述の通りですが、malliでの型定義は関数のDocstringには載ってきません。これは不便なことが多いのでDocstring参照時にmalliの型定義も表示できるようにするelin.interceptor.nrepl.malli/lookup-schemaというインターセプターが用意されています。
e.g. https://bsky.app/profile/uochan.bsky.social/post/3kmq4mkoomy2d
プラグイン
Elinは外部のハンドラー・インターセプターをプラグインとして取り込むこともサポートしています。
いくつかの機能はすでにプラグインとして公開しており、それらはGitHub上でelin-clj-pluginというトピックでまとめています。
独自プラグインの作り方はここでは解説しませんが、プラグインであることの判定のためのファイルをClojureコードを書けばいいだけなので敷居は低いと思っているので、もし作った方がいたらぜひこのトピックを設定していただけたらなと思います。
例として1つコードフォーマッティングに関するプラグインを紹介します。
Elin本体にはあえてコードフォーマッティングに関する機能は持たせていません。
コードフォーマッティングはチーム開発をする上ではCIでのチェックが必須で、そうすると開発環境の機能として提供しても基本的に使わない(使うとしてもClojureをとりあえず試したい人が使うくらい)であるためです。
そのためElinでは外部コマンド(e.g. cljfmt, cljstyle)を使ってElin外でコードフォーマッティングを行ってもらうことを基本としていますが、1ファイル内のコード量が多い場合などはフォーマッティングに時間がかかってしまってエディタ上の操作を妨げることがあります。
そのようなケースに向けたのがこのプラグインで、これはカーソル配下のトップレベルの式だけを対象としてフォーマッティングを行うものです。
実際のフォーマッティングには外部コマンドを使っていて、どのコマンドをどのように使うかは設定ファイルで指定できるようになっています。
;; cljstyleでフォーマットする例
{:interceptor {:uses [elin-format.core/format-current-form-interceptor {:command ["cljstyle" "pipe"]}]}}
;; standard-cljでフォーマットする例
{:interceptor {:uses [elin-format.core/format-current-form-interceptor {:command ["standard-clj" "fix" "-"]}
設定
Elinサーバー上でどのようなハンドラーがあり、どのようなインターセプターが動いているかはすべて設定ファイルで管理されています。
Elinのポリシーの1つに「すべての設定は明示的に記述する」というものがあり、ハンドラー・インターセプターも例外ではありません。
同時にElinではユーザー/プロジェクト単位での設定も勿論サポートしていて、どのハンドラーを使う/使わない、どのインターセプターをいつ使う/使わないなど設定の上書きができます。
つまりElin本体で定義されているハンドラー・インターセプターはすべて置き換え可能であり、やろうと思えばElinのデフォルトの挙動をすべて変更することも可能です。
Elin自体の開発
Elin にベースの機能が実装できて以降は Elin 自体も Elin を使って開発しています。
この開発の構成は自分でもなかなかおもしろいと思っているので簡単に説明します。
開発用Elinサーバー
ElinサーバーはBabashkaで動くので開発時はBabashkaでnREPLサーバーを立ち上げて開発を行います。
その際 Elin では bb nrepl-server
をオーバーライドしてnREPLサーバーを立ち上げると共にその中でElinサーバーを起動するようにしています。
そうするとエディタ上からはnREPLサーバー内で起動しているElinサーバーに接続することができ、そのElinサーバーから自分が動いているnREPLサーバーに接続(!)して開発を行うことができます。
これの何が面白いかというとElin上でElinサーバーで使っている関数を評価すると同じnREPLサーバー上で動いているElinサーバーにもその影響があるので、開発環境をREPL駆動でリアルタイムに更新しながら開発できるということです。
これはClojureの開発環境をClojureで書いているからこそできることであり、Elin自身の開発体験を非常に向上させてくれています。
一方で評価時のエラーが直Elinサーバーに影響して動作がおかしくなるというデメリットもありますが、それを凌ぐメリットを感じているので現状ではこの構成で開発を行っています。
最後に
ここまででElin誕生の経緯、Elinの仕組み、Elin自体の開発について説明してきました。
この記事では使い方についてはあえて触れていませんが、私自身はElinを使って普段の業務をこなしていてそれなりの生産性を出せていると思っている(思い込んでいる)ので、業務レベルでも問題ない範囲の機能は提供できているかと思っています。
現状では Work in progress
というステータスですが、前述の通り業務でも使っているレベルなので近いうちに 1.0.0
をリリースしたいなと思っています。(そのためのドキュメントが不足しているのが目下の課題です)
Elinの仕組みや開発に興味をもった方は勿論、Vim/NeovimでのClojureを使った開発においてより効率的な開発をしたいと思っている方は是非一度試してみていただければなと思います。
Discussion