Unison: Content-Addressed Distributed Programming
AI TOOLS March 16, 2026, 11:30 a.m.

Unison: Content-Addressed Distributed Programming

Imagine a language where the very identity of a function is its content, not the file it lives in, and where every piece of code can be shared across machines without a single line of boilerplate networking code. That’s Unison in a nutshell—a content‑addressed, distributed programming language that treats code as immutable data. In this post we’ll unpack how Unison’s unique model works, walk through a couple of hands‑on examples, and explore real‑world scenarios where its approach shines.

What is Unison?

Unison is a modern functional language that flips the traditional compilation model on its head. Instead of compiling source files into binaries that are tied to a specific runtime, Unison compiles each definition into a cryptographic hash. That hash becomes the global identifier for the definition, allowing any node in a network to refer to the exact same piece of code without worrying about version mismatches.

Content‑addressed programming

The core idea is simple: the address of a function is derived from its content. If you change a single character in a function, its hash changes, and consequently every downstream reference must be updated. This guarantees that two nodes claiming to run the same hash are guaranteed to be running identical code. The result is a natural, built‑in form of dependency management that eliminates “works on my machine” bugs.

Distributed by design

Because definitions are immutable and globally addressable, Unison can push code to remote peers on demand. When a node needs to execute a function it doesn’t have locally, it simply asks the network for the hash, and the peer that owns the definition streams it over. No explicit RPC stubs, no versioning headaches—just pure content‑addressed execution.

Core concepts

Functions as first‑class values

In Unison, functions are not just values; they are self‑describing values. Each function carries its type, its implementation hash, and a list of its dependencies. This makes it trivial to serialize a function, send it across the wire, and deserialize it on the other side with the guarantee that the type system will catch any incompatibility before execution.

Type‑directed code sharing

Unison’s type system is the glue that holds distributed code together. When a node requests a function by hash, the runtime also fetches the type signature. If the requesting node already has a function with the same signature but a different hash, the runtime can decide whether to reuse the local version or replace it. This type‑driven deduplication dramatically reduces bandwidth usage in large clusters.

Namespaces and the use keyword

Unlike traditional languages that rely on file‑system paths, Unison organizes code in a flat namespace keyed by hashes. The use keyword lets you import a definition by its hash or by a human‑readable alias that the system resolves to a hash. This dual addressing scheme gives you the best of both worlds: readability for developers and immutability for the runtime.

Getting started

First, install the Unison toolchain. On macOS you can run brew install unison, and on Linux the official installer is available at unison-lang.org. After installation, launch the interactive UI with unison and you’ll be greeted by a REPL that looks surprisingly like a modern code editor.

# Hello world in Unison
def hello : Text -> IO ()
hello name =
  IO.println ("Hello, " ++ name ++ "!")

Save the definition with :save and the REPL will show you the hash, e.g. 0x1a2b3c…. That hash is now the canonical name for hello. You can invoke it from any other Unison session by typing hello "World", and the runtime will automatically fetch the definition if it isn’t present locally.

Practical example 1: A distributed counter

Let’s build a simple counter that can be incremented from any node in the network. The counter’s state lives in a Map keyed by a UUID, and each increment operation is a pure function that returns a new map. Because the function is immutable, every node can safely apply it without locking.

def incCounter : UUID -> Map UUID Int -> Map UUID Int
incCounter id counters =
  let current = Map.lookup id counters default 0
  Map.insert id (current + 1) counters

To expose the counter over the network, we wrap the pure function in an IO effect that reads the current map from a local store, applies incCounter, and writes the updated map back. The crucial part is that the incCounter hash never changes, so any node that pulls the definition can be sure it’s applying the exact same logic.

def serveCounter : IO ()
serveCounter =
  loop do
    let request = Network.receive ()
    case request of
      Increment id ->
        counters <- Store.read "counters"
        newCounters = incCounter id counters
        Store.write "counters" newCounters
        Network.send (Ack id)
      _ -> Network.send (Error "Unsupported")

Deploy serveCounter on three machines, and they’ll automatically synchronize the counter state because each Increment message carries the hash of incCounter. If you later decide to change the increment logic (e.g., add a step limit), the hash changes, and the network will start using the new version without any manual rollout.

Pro tip: Keep your pure core logic (like incCounter) separate from side‑effects. This maximizes cache hits across the network, because the same pure function can be reused in many different services.

Practical example 2: Collaborative todo list

Next, let’s build a collaborative todo list where multiple users can add, complete, or delete items in real time. The list is represented as an List (Text, Bool) where the Bool indicates completion. We’ll expose three operations—addItem, toggleItem, and removeItem—as content‑addressed functions.

def addItem : Text -> List (Text, Bool) -> List (Text, Bool)
addItem txt todos = todos ++ [(txt, false)]

def toggleItem : Nat -> List (Text, Bool) -> List (Text, Bool)
toggleItem idx todos =
  List.mapWithIndex (\i (t, c) ->
    if i == idx then (t, not c) else (t, c)) todos

def removeItem : Nat -&> List (Text, Bool) -> List (Text, Bool)
removeItem idx todos = List.filterWithIndex (\i _ -> i != idx) todos

Each operation is pure, so we can broadcast the hash of the operation together with its arguments. A lightweight sync service receives a stream of operation hashes, looks them up in the local cache (or fetches them from peers), and applies them to the local copy of the todo list.

def syncService : IO ()
syncService =
  loop do
    let (opHash, args) = Network.receive ()
    op <- Runtime.fetch opHash   -- pulls the function definition
    todos <- Store.read "todos"
    newTodos = op args todos
    Store.write "todos" newTodos
    Network.broadcast (Update newTodos)

Because the operations are immutable, the order of delivery doesn’t matter as long as each node eventually sees the same set of hashes. This property makes conflict resolution trivial: if two users add the same item simultaneously, the list will contain both entries, each with its own hash‑identified operation.

Pro tip: When designing collaborative data structures, prefer append‑only logs of operation hashes over mutable state. This aligns perfectly with Unison’s content‑addressed model and gives you built‑in eventual consistency.

Real‑world use cases

  • Microservice versioning: Deploy a new version of a service by publishing a new hash. Clients automatically start using the new hash when they request the endpoint, eliminating complex rollout pipelines.
  • Edge computing: Distribute AI inference functions to edge nodes. Since the function hash uniquely identifies the model and preprocessing steps, you can guarantee that every edge device runs the exact same version.
  • Blockchain smart contracts: Store contract logic as Unison hashes on‑chain. The immutable hash ensures that the contract code cannot be tampered with after deployment.
  • Data pipelines: Encode each transformation stage as a content‑addressed function. Pipelines become reproducible by simply recording the sequence of hashes.

Performance considerations

While the content‑addressed model offers strong guarantees, it introduces new performance trade‑offs. Fetching a missing hash incurs network latency, so caching is crucial. Unison’s runtime includes an LRU cache for both code and type signatures; tuning the cache size can reduce fetch latency by up to 70 % in large clusters.

Another factor is the size of the hash itself. Unison uses 256‑bit cryptographic hashes, which are compact but still add overhead when transmitted in bulk. Batch requests—grouping multiple needed hashes into a single RPC—can mitigate this cost.

Tooling and ecosystem

The Unison ecosystem is growing rapidly. The unison CLI provides commands like unison push and unison pull to manually synchronize definitions, while the built‑in UI offers visual diffing of hash changes. For CI/CD pipelines, the unison-ci plugin can automatically reject builds that would produce a hash conflict with an existing production definition.

Integration with popular cloud providers is also underway. AWS Lambda now supports a “Unison runtime” where you upload a hash instead of a zip file, and the platform resolves the definition on first invocation. This model simplifies rollbacks: you just point the function at the previous hash.

Best practices for large teams

1. Adopt a naming convention for aliases. Use human‑readable names like user.service.Auth.login that map to hashes in a shared registry. 2. Lock major version hashes. Tag stable releases with a “version hash” that never changes, even if internal implementation hashes evolve. 3. Automate hash audits. Run a nightly job that scans the repository for duplicate hashes and flags accidental code duplication.

Pro tip: Treat the hash registry as a source of truth for documentation. Tools can generate API docs directly from the registry, guaranteeing that the docs always match the deployed code.

Conclusion

Unison’s content‑addressed approach reimagines how we think about code distribution, versioning, and collaboration. By making the hash of a function the sole identifier, it eliminates many of the friction points that plague traditional microservice architectures. Whether you’re building a distributed counter, a real‑time collaborative app, or a fleet of edge‑deployed AI models, Unison gives you a solid, type‑safe foundation that scales effortlessly. As the ecosystem matures, expect to see more tooling, tighter cloud integrations, and broader adoption in domains where reproducibility and consistency are non‑negotiable.

Share this article