TL;DR

  • Unison は純粋関数型のプログラミング言語で、Algebraic Effects(Ability)で副作用を書く。
  • ソースファイルを持たず、コードベースを AST のハッシュで管理する。コードの追加や Git 相当の操作は、すべて ucm(Unison Codebase Management)で行う。

背景

ある動画を見ていたら、知らない言語が 2 つ出てきた。Roc と Unison である。少し調べると、Roc はほかの関数型プログラミング言語とそれほど違わなかった。一方、Unison はこれまでのプログラミング言語や開発スタイルをアンラーンする必要があるくらい、根本から違っていた。そこで、とりあえず学んでみることにした。

検証

言語の特徴

Unison は純粋関数型のプログラミング言語で、Haskell や Python のようにインデントでブロックを表す構文を持つ。

double : Nat -> Nat
double x =
  x * 2
use List ++ +: :+
[0, 1, 2, 3] ++ [4, 5]
head = 1 +: [2, 3, 4]
head :+ 5

型クラスやモナドの概念はない。代わりに Algebraic Effects を採用していて、Unison ではこれを Ability と呼ぶ。副作用も Ability で書く。Algebraic Effects は、OCaml、Eff、Koka などの言語でも採用されている。

評価戦略は基本的に正格評価だが、遅延評価もできる。型の前の ' が遅延評価を表し、do はそれ以降を遅延して計算するキーワードである。

helloWorld : '{IO, Exception} ()
helloWorld = do printLine "Hello World"

Elixir のようなパイプ演算子(|>)もある。

Nat.range 0 10
  |> List.map (x -> x Nat.* 100)
  |> List.filter (const true)
  |> List.foldLeft (Nat.+) 0
Optional.filter Nat.isEven <| Some 5

純粋関数型なので、依存している実装が変わっていない限り、テストの結果がキャッシュされる。テストのキャッシュは Go などでも採用されている。テスティングフレームワークも言語に含まれていて、プロパティベーステストも書ける。標準ライブラリ(lib/base)の機能は豊富で、@unison/http のような公式のライブラリも多い。

コードベースをハッシュで管理する

Unison は、コードベース(AST)をハッシュで管理する。たとえば次の increment 関数は、

increment : Nat -> Nat
increment x = x + 1

次のような形で管理される。

increment = (#arg1 -> #a8s6df921a8 #arg1 1)

ソースファイルは存在しないので、コードベースを自分で直接管理することはできない。一時的なバッファである .u ファイルにコードを書き、ucm を使ってコードベースに追加する。コードの管理はすべて ucm を介して行い、Git や GitHub の代わりも ucm が担う。この仕組みによって、変更管理、依存関係の解決、分散実行が容易になり、最低限のインクリメンタルビルドもできる。詳しくは The big technical idea にある。

サンプルコード

パターンマッチは match ... with で書く。cases はその糖衣構文である。

isEven num = match num with
  n | mod n 2 === 0 -> "even"
  _ -> "odd"
 
isEven = cases
  n | mod n 2 === 0 -> "even"
  _ -> "odd"
match Some 12 with
  Optional.None -> "none"
  -- ガードパターン
  Some n| Nat.isEven n  -> "n is a variable and | is a pattern guard"
  -- as パターン
  opt@(Some n) -> "opt binds to the entire optional value"

型宣言では、直和型と直積型(代数的データ型)を書ける。

type LivingThings
  = Animal | Plant | Fungi | Protists | Monera
 
type Pet = {
  age : Nat,
  species : Text,
  foodPreferences : [Text]
}

例外も Ability で扱う。

nonZero : Nat ->{Exception} Nat
nonZero =
  use Nat ==
  cases
    n
      | n == 0 -> Exception.raise (Generic.failure "Zero was found" n)
      | otherwise -> n
 
catch do nonZero 0

副作用のある処理の例を示す。

getRandomElem : [a] ->{Abort, Random} a
getRandomElem list =
  use unison_base_4_8_0.abilities.Random natIn
  randomIndex = natIn 0 (List.size list)
  List.at! randomIndex list
 
toOptional! do splitmix 42 do getRandomElem [1, 2, 3]

toOptional! と splitmix が、Ability を処理するハンドラである。toOptional! : '{g Abort} a -> {g} Optional a は、Abort を Optional に変換する。splitmix : Nat ->'{g, Random} t -> {g} t は、受け取ったシードをもとに乱数を生成する。

Ability は次のように定義する。structural は構造的型付けで型を定義するキーワードで、通常は名前的型付けになる。

structural ability Abort where
  abort : {Abort} a
 
structural ability Exception where
  raise : Failure ->{Exception} x

HTTP クライアント(@unison/http)を使う例である。

exampleGet : '{IO, Exception, Threads} HttpResponse
exampleGet _ =
  use unison_base_4_8_0.IO.net.URI parse
  uri = parse "https://share.unison-lang.org/@unison/httpclient"
  req = do Http.get uri
  Http.run req
 
printResponse : '{IO, Exception} ()
printResponse =
  use unison_base_4_8_0.abilities Random.run
  use systemfw_concurrent_7_2_1 Threads.run
  do
    Random.run do
      Threads.run do
        response = exampleGet ()
        printLine (bodyText response)

ucm

ucm は、Git 相当の操作、依存関係のインストール、コード検索など、あらゆる操作を行う対話型の CLI である。Git と Lisp の REPL を合わせたものをイメージするとよい。GitHub に相当するサービスとして Unison Share があり、Unison Share への push なども ucm で行う。ucm を実行すると立ち上がる。

主なコマンドは次のとおり。

  • help: ucm のコマンド一覧
  • auth.login: ucm と Unison Share のアカウントを連携する
  • プロジェクト
    • projects / project.list: プロジェクトの一覧
    • project.create / project.delete: プロジェクトの作成と削除
    • switch: プロジェクトの切り替え
    • lib.install: サードパーティのライブラリのインストール
    • ls: 名前空間の一覧
  • コードベース
    • view: 指定した名前の定義を表示する。Unison Desktop を使えば GUI でも見られる
    • add / update: 一時バッファのコードをすべてコードベースに反映する。--- より下の行は無視される
    • delete: 指定した定義を削除する
    • edit: 指定した名前の定義を、編集のために一時バッファにコピーする
    • reflog: 現在のブランチの変更履歴を表示する
    • find: コードベースで定義されている型や関数を検索する
    • docs: コードのドキュメントを見る
  • ブランチ
    • branches / branch.list: ブランチの一覧
    • merge: ブランチのマージ
  • 実行
    • run: 指定した {IO, Exception} () の関数を実行する
    • compile: 指定した '{IO, Exception} () の関数をコンパイルし、実行ファイル(.uc)を生成する。生成したファイルは ucm run.compiled で実行できる
  • テスト
    • test: テストを実行する

デプロイと教材

Unison Cloud を使うと、ucm から直接デプロイできるようである(HTTP hello world、未調査)。Docker イメージも提供されていて、compile で生成した実行ファイルを ucm run.compiled で実行できるので、AWS などの一般的な PaaS でも動かせると思う。

学ぶ教材としては、公式のチュートリアルと Exercism がある。Web アプリケーションを作りたいなら、Unison Cloud のチュートリアルがある。開発の進め方は、公式ドキュメントの Project workflows が参考になる。update した実装は、デスクトップアプリで確かめられる。

考察

新しいプログラミング言語の可能性を見られた。一方で、AI に書かせると Haskell のコードを補完しがちだった。ucm の操作をツールとして AI に認識させればうまくいくかもしれない。有志が MCP サーバ(mizchi/unison-mcp)を開発している。

インタフェースが AI エージェントで、内部では Unison のようにコードベースを管理する言語というのは面白そうである。ハッシュで管理するのが最善の解決策なのかは分からない。

キーワード

参考