iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🤖

Exploring the "Next" Programming Language: Insights from Unison

に公開

Since Functional Programming Matsuri, I've been curious about the Unison language.

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

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.

https://fsharpforfunandprofit.com/rop/

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.u is 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.
  • 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 do notation.
    • 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.

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

brew tap unisonweb/unison
brew install unison-language

Also, install the VS Code extension:

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

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

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

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.

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

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