関数型まつり以降、Unison 言語が気になっています。
最近、試しにAI用のLSPとか作ってたんですが、既存のプログラミング言語処理系にやや限界を感じています。既存言語の人間用のファイル参照と line:character の補完というインターフェースが、AIの操作単位と噛み合ってないじゃないか?という懸念です。
関数型まつりでも発表があった、関数型ドメインモデリングの Scott Wlaschin 氏いわく「副作用のない関数だけでプロジェクトを構成すれば、関数名はただの名前空間のルックアップテーブルに過ぎなくなり、その合成だけでドメインを表現できる」とし、関数を組み合わせるスタイルの Railway Oriented Programming を提唱していました。
関数型まつりで Unison 関連の発表があったわけではないんですが、関数型言語のうち自分がほしい特性をAIに相談したら、それって Unison じゃない? と提案されたことがきっかけです。
(二次会で話してた https://x.com/yukikurage_2019 にも、それって Unison では?と言われたのがあります)
Unison は既存のエディタやテキスト表現を前提としない、まったく新しい体系のプログラミング言語です。実際に Unison が使い物になるかと言うかどうかというのは一旦置いといて(マイナー言語なのは間違いないです)、新しい概念を掴むために Unison 言語を少し勉強することにしました。
このブログはその学習ログです。
最初に: 頭をリセットする
Unison 言語は一般的なプログラミング言語と全く違うワークフローを要求します。
まず純粋関数型+エフェクトシステムのある言語ということで、一般的なプログラマの発想から遠いのとは別に、さらに独自のコード管理とバージョン管理を持ちます。
前提が大きく違うので、頭の体操が必要です。
- すべての関数は内部的に自己参照ハッシュで表現されます
- nix のように環境上の参照を固定できます
- 言語自体に Git 相当の機能が組み込まれています
- コードの変更には Unison 言語専用の UCM (Unison Code Manager) を経由します
- すべての関数は自己参照ハッシュで保存されて UCM が追跡します
- ファイルという実体がないです
-
scratch.u
は、編集するものを一時的に展開するバッファに過ぎません
-
- Git に詳しい人向けの説明
- AST から
.git/objects/
相当の blob object を保存/解決します - git のベアリポジトリ相当から始まり、プロジェクト上の名前空間は、単に関数ハッシュへの参照になります
- その実体は
~/.unison/v2/unison.sqlite3
です
- AST から
- 関数型言語に詳しい人向けの説明
- エフェクトシステムが組み込まれた純粋関数型の言語です
- 基本的に Haskell の構文やオフサイドルールに従います。 do 構文もあります
- 純粋関数によって書かれたテストは、その環境では不変なはずなのでキャッシュされます
- 関数型言語としての説明はこの記事ではしません
UCMの何が嬉しいのか
- コードをその時記述して動いたときの状態で、その環境における一意な参照を解決する
- 「あのとき動いていたコード」を探すのが簡単
- 変更時には新しい実装同士で型の整合性をとってコミットする(必要がある)
- 型チェックとバージョン管理が統合されているので、純粋関数である限り不整合が起きない
- 差分検知によるインクリメンタルな型チェックとビルドがある
インストール
いちばん簡単なのは、 homebrew / linuxbrew でインストールすることです。
brew tap unisonweb/unison
brew install unison-language
ついでに vscode 拡張をいれます
Unison 最初の一歩
vscode で適当なプロジェクトを開き、ターミナルで ucm
を起動します。
実際はファイルという実体がないので、どこで ucm を起動しても構いません。
ここでは myplg
というプロジェクト(名前空間)を作成しました。
$ ucm
Now starting the Unison Codebase Manager (UCM)...
scratch/main> project.create myplg
1. Open scratch.u.
2. Write some Unison code and save the file.
3. In UCM, type `update` to save it to your new project.
ヒントにでているように、 scratch.u
というファイルを作成して、動作確認で次のように入力して保存(Ctrl-S) します。
すると、ucm 側で、次のように表示されるはずです
myplg/main>
1 | > 1 + 1
⧩
2
>
が REPL として評価された値が出力されるはずです。
Hello World
scratch.u に、さらに次のようなコードを保存してみましょう。
helloWorld : '{IO, Exception} ()
helloWorld _ = printLine "Hello World"
'{IO, Expeption}
の型表現は Ability といって、その関数の要求する副作用・エフェクトを示しています。 printLine
にホバーすると、次のような型シグネチャとその詳細が表示させるはずです。
printLine : Text ->{IO, Exception} ()
printLine "myText" prints the given Text value to the console.
これで Ctrl-S を押すと、 ucm 側が scratch.u の変更に反応するはずです。
Loading changes detected in ...scratch.u.
I found and typechecked these definitions in
...scratch.u. If you do an `update`, here's how
your codebase would change:
⍟ These new definitions are ok to `update`:
helloWorld : '{IO, Exception} ()
この時点で、 ucm 側で run helloWrold
で実行することができます。
myplg/main> run helloWorld
Hello World
()
とはいえ、まだこのコードはプロジェクトに取り込まれていません。 ucm 側で、update
して、 ls
してみます。
myplg/main> update
Done.
myplg/main> ls
1. helloWorld ('{IO, Exception} ())
2. lib/ (8013 terms, 184 types)
myplg/main> view helloWorld
helloWorld : '{IO, Exception} ()
helloWorld _ = printLine "Hello World"
(lib は標準ライブラリの名前空間です)
これで scratch.u のコードが、myplg/main
に helloWorld
が取り込まれていることが確認できました。
テストを書く
scratch.u で test>
でテストコードを追加できます。
square : Nat -> Nat
square x = x * x
test> square.tests.ex1 = check (square 4 == 16)
面白いのは、Unisonは純粋関数型言語なので、純粋関数のテスト結果はキャッシュされています。
ucm から変更を取り込んで、このテストを実行してみます。
myplg/main> update
myplg/main> test square.tests
Cached test results (`help testcache` to learn more)
1. square.tests.ex1 ◉ Passed
✅ 1 test(s) passing
Tip: Use view 1 to view the source of a test.
myplg/main> ls square.tests
1. ex1 ([Result])
新しいコードを追加する branch.create ~ merge
一度 add/update したら scratch.u から削除しても、その関数は残っています。
別ブランチに移動して、そこでコードを書いて、編集してみます
myplg/main> branches
Branch Remote branch
1. main
myplg/main> branch.create distance
myplg/distance>
次のようなコードを書きます
type Point = {
x : Float,
y : Float
}
distance : Point -> Point -> Float
distance p1 p2 =
dx = abs (Point.x p1 - Point.x p2)
dy = abs (Point.y p1 - Point.y p2)
sqrt (pow dx 2.0 + pow dy 2.0)
test> distance.tests.ex1 = check (distance (Point 3.0 4.0) (Point 0.0 0.0) == 5.0)
これをコミットして merge
myplg/distance> update
myplg/distance> switch /main
myplg/main> merge /distance
I fast-forward merged myplg/distance into myplg/main.
myplg/main> find tests
1. distance.tests.ex1 : [Result]
2. square.tests.ex1 : [Result]
myplg/main> test distance.tests
Cached test results (`help testcache` to learn more)
1. distance.tests.ex1 ◉ Passed
✅ 1 test(s) passing
Tip: Use view 1 to view the source of a test.
square を再編集
scratch.u をまっさらにして、次のコマンドを打ちます。
myplg/main> edit square
☝️
I added 1 definitions to the top of
/home/mizchi/mizchi/zenn/slides/aoai/unison-plg/scratch.u
You can edit them there, then run `update` to replace the
definitions currently in this namespace.
edit square によって、scratch.u は次のようにコードが書き込まれます
square : Nat -> Nat
square x =
use Nat *
x * x
元のコードとは違う点に注意してください。use Nat *
は標準ライブラリを暗に省略していただけで、実際には関数単位の依存として内部的に展開されていました。
ucm ではテキストコードを直接保存しているのではなく、関数単位で他の関数への依存があり、それを edit では展開しているわけです。
Unison の面白さ
人間が書くコードは表象に過ぎず、Git で AST の自己参照ハッシュがある、というのは AI 向けに向いてる抽象だと自分は感じました。テキスト表現なんて人間用のインターフェースで、AI は直接構造化データを参照する方がいいですからね。
他にも、紹介してない機能がたくさんあります。
- Unison Share によるコード共有(GitHub 相当)
- Unison Cloud による分散実行
- Ability によるエフェクトシステム
ただし、Unison には明確な欠点があります。それは Git/GitHub とはまったく異なるワークフローになってしまったことで GitHub にホストされず、それによって AI の学習となっておらず 「なんか Haskell 風の言語」以上の精度がでません。
とはいえ、コンパイラが賢いのと、要はHaskellなので、意外となんとかなってる気がします。最初に既存の言語との差分の、簡単なチートシートを作っておきましょう。
おまけ Unison UCM MCP を作ってみた
面白半分で、 Haskell で UCM を操作する MCP を書かせてみました。
Claude Code に UCM を操作する MCP を提供して、それによってコードを書かせる実験です。
ダイクストラによる経路探索ぐらいだったら、これで実装ができましたが。…まだそれ以上の評価はしていません。一応動いたという感じ。
Haskell で JSONRPCの MCP を喋るサンプルぐらいにはなってると思います。
考察
- テキスト表現は人間用インターフェースに過ぎない
- バイナリで格納して、閲覧時にユーザーの好きなフォーマットで展開すればいいい
- コード検索は、grep ではなく ASTと型システムと合わせてクエリできればいい
- 言語自体がバージョニング管理できれば、AIの書き散らすコードを管理できる
- しかし、そうすると GitHub のインターフェースに乗らなくなる
- 既存の言語に似ていて、コンパイラの警告がある程度賢ければ、学習量の不利は踏み倒せる
- AI用の言語に必要なのは、たぶん専用のシェル統合環境なんじゃないか
- MCPとエフェクトシステムを統合すれば、明示的な副作用の制御ができそう
- ランタイム系は Wasm で作って、 WASI Sandboxで権限制御すればいいじゃないか?
- Unison はまだ人間向きなんじゃないか?
- ucm のシェル相当で長大なワンライナー(人間用ではないので型含めた大量の記述を要求していい)でその時書いたコードをハッシュで保存して、コード片を組み合わせて、最後に可読性のあるコード表現に落とせばいい
というのを余裕があったり作りたく、じつはちょっとだけ試作したんですが、それは後で書きます
Views: 0