金曜日, 7月 18, 2025
金曜日, 7月 18, 2025
- Advertisment -
ホームニューステックニュースUnison 言語から、「次」の言語を考察したい

Unison 言語から、「次」の言語を考察したい



関数型まつり以降、Unison 言語が気になっています。

https://www.unison-lang.org/

最近、試しにAI用のLSPとか作ってたんですが、既存のプログラミング言語処理系にやや限界を感じています。既存言語の人間用のファイル参照と line:character の補完というインターフェースが、AIの操作単位と噛み合ってないじゃないか?という懸念です。

関数型まつりでも発表があった、関数型ドメインモデリングの Scott Wlaschin 氏いわく「副作用のない関数だけでプロジェクトを構成すれば、関数名はただの名前空間のルックアップテーブルに過ぎなくなり、その合成だけでドメインを表現できる」とし、関数を組み合わせるスタイルの Railway Oriented Programming を提唱していました。

https://fsharpforfunandprofit.com/rop/

関数型まつりで 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 です
  • 関数型言語に詳しい人向けの説明
    • エフェクトシステムが組み込まれた純粋関数型の言語です
    • 基本的に Haskell の構文やオフサイドルールに従います。 do 構文もあります
    • 純粋関数によって書かれたテストは、その環境では不変なはずなのでキャッシュされます
    • 関数型言語としての説明はこの記事ではしません

UCMの何が嬉しいのか

  • コードをその時記述して動いたときの状態で、その環境における一意な参照を解決する
    • 「あのとき動いていたコード」を探すのが簡単
    • 変更時には新しい実装同士で型の整合性をとってコミットする(必要がある)
  • 型チェックとバージョン管理が統合されているので、純粋関数である限り不整合が起きない
  • 差分検知によるインクリメンタルな型チェックとビルドがある

インストール

いちばん簡単なのは、 homebrew / linuxbrew でインストールすることです。

https://www.unison-lang.org/docs/install-instructions/

brew tap unisonweb/unison
brew install unison-language

ついでに vscode 拡張をいれます

https://marketplace.visualstudio.com/items?itemName=unison-lang.unison

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/mainhelloWorld が取り込まれていることが確認できました。

テストを書く

https://www.unison-lang.org/docs/usage-topics/testing/

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 を書かせてみました。

https://github.com/mizchi/unison-mcp

Claude Code に UCM を操作する MCP を提供して、それによってコードを書かせる実験です。

ダイクストラによる経路探索ぐらいだったら、これで実装ができましたが。…まだそれ以上の評価はしていません。一応動いたという感じ。

Haskell で JSONRPCの MCP を喋るサンプルぐらいにはなってると思います。

考察

  • テキスト表現は人間用インターフェースに過ぎない
    • バイナリで格納して、閲覧時にユーザーの好きなフォーマットで展開すればいいい
    • コード検索は、grep ではなく ASTと型システムと合わせてクエリできればいい
    • 言語自体がバージョニング管理できれば、AIの書き散らすコードを管理できる
    • しかし、そうすると GitHub のインターフェースに乗らなくなる
  • 既存の言語に似ていて、コンパイラの警告がある程度賢ければ、学習量の不利は踏み倒せる
  • AI用の言語に必要なのは、たぶん専用のシェル統合環境なんじゃないか
    • MCPとエフェクトシステムを統合すれば、明示的な副作用の制御ができそう
    • ランタイム系は Wasm で作って、 WASI Sandboxで権限制御すればいいじゃないか?
  • Unison はまだ人間向きなんじゃないか?
    • ucm のシェル相当で長大なワンライナー(人間用ではないので型含めた大量の記述を要求していい)でその時書いたコードをハッシュで保存して、コード片を組み合わせて、最後に可読性のあるコード表現に落とせばいい

というのを余裕があったり作りたく、じつはちょっとだけ試作したんですが、それは後で書きます



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -