iTranslated by AI
Exploring the "Next" Programming Language: Insights from Unison
Since Functional Programming Matsuri, I've been curious about the Unison language.
Recently, I've been trying to create an LSP for AI as an experiment, but I'm feeling some limitations with existing programming language processing systems. I'm concerned that the interface of existing languages—human-oriented file references and line:character completions—might not be aligned with the units of operation for AI.
According to Scott Wlaschin, the author of "Functional Design in F#" and speaker at Functional Programming Matsuri, "if you compose a project only with side-effect-free functions, function names become nothing more than a namespace lookup table, and you can represent a domain just by composing them." He advocated for "Railway Oriented Programming," a style of combining functions.
It's not that there was a specific presentation about Unison at Functional Programming Matsuri, but when I consulted an AI about the characteristics I wanted in a functional language, it suggested, "Isn't that Unison?" That was the trigger.
(I was also told "Isn't that Unison?" by @yukikurage_2019 while talking at the after-party: https://x.com/yukikurage_2019)
Unison is a completely new paradigm of programming language that doesn't assume existing editors or text representations. Putting aside whether Unison is actually practical for now (it's certainly a niche language), I decided to study Unison a bit to grasp its new concepts.
This blog is my learning log.
Initially: Reset your mind
The Unison language requires a workflow completely different from typical programming languages.
First, besides being a pure functional language with an effect system—which is already far from a general programmer's mindset—it has its own unique code and version management.
Because the premises are so different, a bit of mental gymnastics is required.
- All functions are represented internally by self-referential hashes.
- Like Nix, you can pin references in the environment.
- Features equivalent to Git are built into the language itself.
- Code changes go through UCM (Unison Code Manager), specific to the Unison language.
- All functions are saved as self-referential hashes and tracked by UCM.
- There is no physical "file" entity.
-
scratch.uis merely a buffer to temporarily expand what you are editing.
-
- For those familiar with Git:
- It saves/resolves blob objects equivalent to
.git/objects/from the AST. - Starting from something like a bare Git repository, namespaces in the project simply become references to function hashes.
- The actual entity is
~/.unison/v2/unison.sqlite3.
- It saves/resolves blob objects equivalent to
- For those familiar with functional languages:
- It's a pure functional language with a built-in effect system.
- It basically follows Haskell's syntax and off-side rule. It also has
donotation. - Tests written with pure functions are cached since they should be immutable in that environment.
- I won't go into detail about it as a functional language in this article.
What's great about UCM
- It resolves a unique reference in that environment based on the state of the code when it was written and worked.
- Makes it easy to find "the code that was working back then."
- When making changes, you (need to) commit after ensuring type consistency between new implementations.
- Since type checking and version control are integrated, inconsistencies won't occur as long as they are pure functions.
- It features incremental type checking and builds via difference detection.
Installation
The easiest way is to install via homebrew / linuxbrew.
brew tap unisonweb/unison
brew install unison-language
Also, install the VS Code extension:
Unison: First Steps
Open any project in VS Code and start ucm in the terminal.
Actually, since there is no physical file entity, you can start ucm anywhere.
Here, I created a project (namespace) called 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.
As suggested in the hint, create a file named scratch.u, input the following to check the operation, and save (Ctrl-S).
> 1 + 1
Then, you should see something like this on the ucm side:
myplg/main>
1 | > 1 + 1
⧩
2
The value evaluated by > acting as a REPL should be output.
Hello World
Let's save the following code to scratch.u.
helloWorld : '{IO, Exception} ()
helloWorld _ = printLine "Hello World"
The type expression '{IO, Exception} is called an Ability, which indicates the side effects or effects required by that function. If you hover over printLine, you should see the following type signature and its details:
printLine : Text ->{IO, Exception} ()
printLine "myText" prints the given Text value to the console.
When you press Ctrl-S, the ucm side should respond to the changes in 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} ()
At this point, you can execute it with run helloWorld on the ucm side.
myplg/main> run helloWorld
Hello World
()
However, this code hasn't been incorporated into the project yet. Let's update on the ucm side and try 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 is the namespace for the standard library.)
This confirms that the code from scratch.u has been incorporated as helloWorld into myplg/main.
Writing Tests
You can add test code in scratch.u using test>.
square : Nat -> Nat
square x = x * x
test> square.tests.ex1 = check (square 4 == 16)
What's interesting is that because Unison is a pure functional language, the test results for pure functions are cached.
Let's incorporate the changes from UCM and run this test.
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])
Adding New Code: branch.create ~ merge
Once you have performed an add/update, the function remains even if you delete it from scratch.u.
Let's move to a different branch and try writing and editing code there.
myplg/main> branches
Branch Remote branch
1. main
myplg/main> branch.create distance
myplg/distance>
Write the following code:
-- https://www.unison-lang.org/docs/language-reference/record-type/
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)
Commit this and 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.
Re-editing square
Clear scratch.u and run the following command.
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.
With edit square, the following code is written into scratch.u:
square : Nat -> Nat
square x =
use Nat *
x * x
Note that it differs from the original code.
use Nat * was simply omitting the standard library implicitly; in reality, it was expanded internally as a per-function dependency.
In UCM, instead of saving text code directly, functions have dependencies on other functions, which edit expands.
The fascination of Unison
I felt that the abstraction where the code written by humans is merely a representation, and the codebase has the self-referential hash of the AST, is an abstraction well-suited for AI. Text representation is just an interface for humans, and it's better for AI to refer to structured data directly.
There are many other features I haven't introduced yet:
- Code sharing via Unison Share (equivalent to GitHub)
- Distributed execution via Unison Cloud
- Effect system via Ability
However, Unison has a clear drawback. Because it has a completely different workflow from Git/GitHub, it isn't hosted on GitHub, and as a result, it hasn't been part of AI training data, so the accuracy isn't much better than "some Haskell-like language."
That said, since the compiler is smart and it is essentially Haskell, I feel like it's working out surprisingly well. Let's create a simple cheat sheet of the differences from existing languages first.
Bonus: I made a Unison UCM MCP
For fun, I had it write an MCP that operates UCM using Haskell.
This is an experiment to provide Claude Code with an MCP that operates UCM and have it write code using it.
I managed to implement something like pathfinding using Dijkstra's algorithm with this. ...I haven't evaluated it beyond that yet. It feels like "it worked for now."
I think it serves as a decent sample for implementing a JSON-RPC MCP in Haskell.
Reflections
- Text representation is merely a human interface.
- We should store code in binary and expand it into the user's preferred format when viewing.
- Code search should be a query combined with the AST and type system, rather than just
grep. - If the language itself can handle versioning, we can manage the code churned out by AI.
- However, doing so means losing the GitHub interface.
- If the language resembles existing ones and the compiler warnings are reasonably smart, the disadvantage in the volume of training data can be overcome.
- What an AI-oriented language needs is likely a dedicated shell integration environment.
- Integrating MCP and the effect system would allow for explicit control of side effects.
- Why not build the runtime in Wasm and control permissions with a WASI Sandbox?
- Is Unison still intended for humans?
- At the UCM shell level, we could save code fragments as hashes via long one-liners (which can require extensive descriptions including types since they aren't for humans), combine these fragments, and finally output them as a readable code representation.
I'd like to create something like this if I have the time, and I've actually made a small prototype, but I'll write about that later.
Discussion