This document walks through the basics of using the Unison codebase manager and writing Unison code. We will introduce bits and pieces of the core Unison language and its syntax as we go. The Unison language documentation is a more in-depth resource if you have questions or want to learn more.
If you want to follow along with this document (highly recommended), this guide assumes you've already gone through the steps inthe quickstart guideand skimmed throughthe big idea.
👋 to the Unison codebase manager
The Unison Codebase Manager, or UCM for short, is the command line tool that runs the Unison programming language and allows you to interact with the Unison code you've written and saved. Put differently, the UCM is the interface to your Unison codebase.
💡 Remember: Unison code is not saved as text-based file content. Because of this, we need a tool that lets us change and run Unison programs.
Its many responsibilities include:
- Typechecking and compiling new code
- Organizing, navigating, and finding Unison definitions
- Storing the state of your codebase
- Running Unison programs and Unison binaries
- Publishing and pulling Unison libraries
🎉 Running the UCM
By default, running
ucmin a directory will interact with any
.usuffixed file in the directory where the command was issued while opening the default codebase in your home directory. You'll get a message in the UCM like:
Now starting the Unison Codebase Manager (UCM)... […] 🐣 Since this is a fresh codebase, let me download the base library for you.
What's happening here? This is the Unison Codebase Manager starting up and initializing a fresh codebase with the standard library. We're used to thinking about our codebase as a bag of text files that's mutated as we make changes to our code, but in Unison the codebase is represented as a collection of serialized syntax trees, identified by a hash of their content and stored in a collection of files inside of a
.unisondirectory in the path you supplied to the ucm.
The Unison codebase format has a few key properties:
- It isappend-only:once a file in the
.unisondirectory is created, it is never modified or deleted, and files are always named uniquely and deterministically based on their content.
- As a result, a Unison codebase can be versioned and synchronized with Git or any similar tool and will never generate a conflict in those tools.
Let's explore the
baselibrary that was just downloaded and get used to navigating a Unison codebase.
You can view the terms and types in a namespace with the
.> ls base.data.List
The output should be a numbered list of definitions and their associated signatures.
.> ls base.data.List 1. ++ ([a] -> [a] -> [a]) 2. +: (a -> [a] -> [a]) 3. :+ ([a] -> a -> [a]) 4. Nonempty (type) […]
Because of the append-only nature of the codebase format, we can cache all sorts of interesting information about definitions in the codebase andnever have to worry about cache invalidation.For instance, Unison is a statically-typed language and we know the type of all definitions in the codebase—the codebase is always in a well-typed state. So one thing that's useful and easy to maintain is an index that lets us search for definitions in the codebase by their type. Try out the following commands (new syntax is explained below):
.> find : [a] -> [a] 1. base.data.List.distinct : [a] -> [a] 2. base.data.Heap.sort : [a] -> [a] 3. base.data.List.dropLast : [a] -> [a] 4. base.data.List.reverse : [a] -> [a] 5. base.data.Heap.sortDescending : [a] -> [a] .> view 4 base.data.List.reverse : [a] -> [a] base.data.List.reverse as = List.foldLeft (acc a -> a +: acc)  as
Here, we used the did a type-based, with
findfollowed by a colon,
:,to search for functions of type
[a] -> [a].We got a list of results, and then used the
viewcommand to look at the nicely formatted source code of one of these results. Let's introduce some Unison syntax:
List.reverse : [a] -> [a]is the syntax for giving a type signature to a definition. We pronounce the
:symbol as "has type", as in reverse has the type
[a] -> [a].
[Nat]is the syntax for the type consisting of lists of natural numbers (terms like
[0, 1, 2]and
[3, 4, 5],and
will have this type), and more generally
[Foo]is the type of lists whose elements have some type
- Any lowercase variable in a type signature is assumed to beuniversally quantified,so
[a] -> [a]really means and could be written
forall a . [a] -> [a],which is the type of functions that take a list whose elements are some (but any) type, and return a list of elements of that same type.
List.reverse astakes one parameter, called
as.The stuff after the
=is called thebodyof the function, and here it's ablock,which is demarcated by whitespace.
acc a -> ..is the syntax for an anonymous function.
- Function arguments are separated by spaces and function application binds tighter than any operator, so
f x y + g p qparses as
(f x y) + (g p q).You can always use parentheses to control grouping more explicitly.
view base.data.List.foldLeftif you're curious to see how it's defined.
🖼 Check out the local codebase UI
We've been navigating the codebase via the UCM command line, but there's another option for exploring and viewing a Unison codebase: the Unison codebase UI.
The codebase UI is a graphical interface for exploring your codebase. You can search for terms, click-through to Unison code definitions, and read code documentation.
uiin the UCM to open the local UI and search for
Text.dropRightWhile.Unison docs are automatically linked to the term and the source code is available for exploration.
Names are stored separately from definitions so renaming is fast and 100% accurate
The Unison codebase, in its definition for
List.reverse,doesn't store names for the definitions it depends on (like the
List.foldLeftfunction); it references these definitions via their hash. As a result, changing the name(s) associated with a definition is easy.
Let's try this out.
List.reverseis defined using
List.foldLeft.Let's rename that to
List.foldlto make it more familiar to Haskell fans. Try out the following command (you can use tab completion here if you like):
.> move.term base.data.List.foldLeft base.data.List.foldl Done. .> view base.data.List.reverse base.data.List.reverse : [a] -> [a] base.data.List.reverse as = use base.data.List +: base.data.List.foldl (acc a -> a +: acc)  as
foldlname now, so the rename has taken effect. Nice!
To make this happen, Unison just changed the name associated with the hash of
List.foldLeftin one place.The
viewcommand looks up the names for the hashes on the fly, right when it's printing out the code.
This is important: Unisonisn'tdoing a bunch of text mutation on your behalf, updating possibly thousands of files, generating a huge textual diff, and also breaking a bunch of downstream library users who are still expecting that definition to be called by the old name. That would be ridiculous, right?
So rename and move things around as much as you want. Don't worry about picking a perfect name the first time. Give the same definition multiple names if you want. It's all good!
The fact that Unison codebases are immutable and append-only means that we can "rewind" our codebase to an earlier point in time. Use the
reflogcommand to see a log of the codebase changes. You should see some help text and a numbered list of hashes.
1. #2cbugd57qa : move.term .base.data.List.foldLeft .base.data.List.foldl 2. #na6fel77ai : pull unison.public.base.latest .base 3. #sjg2v58vn2 : (initial reflogged namespace)
Reflog keeps track of the history of the codebase by recording the hash of the rootnamespaceof your codebase. Namespace hashes change along with updates to the term and type definitions that they enclose. When we renamed
List.foldLeft,conceptually, the "state" of the codebase changed, but the log-based format of the codebase history means those changes are retrievable.
Let's try to undo the rename action. Use the
reset-rootcommand to pick a prior codebase state to return to. We'll give it the hash of the codebase from just before the
move.termcommand was issued.
.> reset-root #na6fel77ai Done.
Great! OK, go drink some water, 🚰 and then let's learn more about Unison namespaces and the expected codebase structure.
Namespace conventions and searching the UCM
Thus far we've been exploring the
basestandard library that automatically downloads into the root of your codebase, but when you're writing your own Unison code, you won't want to add your functions and terms at the root. Instead, let's introduce the concept of "namespaces" by creating a
tournamespace and establishing some conventions for organizing it.
Unisonnamespacesare mappings from names to definitions. Names in Unison look like this:
foo.That is: an optional
.,followed by one or more segments separated by a
.,with the last segment allowed to be an operator name like
We often think of these names as forming a tree, much like a directory of files, and names are like file paths in this tree.Absolute names(like
.base.Int)start with a
.and are paths from the root of this tree andrelativenames (like
math.sqrt)are paths starting from the current namespace, which you can set using the
In the codebase manager console, create a
tournamespace with the
cdcommand. This is where you'll adding code for the remainder of this guide.
.> cd tour
Notice the prompt changes to
.tour>,indicating your current namespace is now
.tour.When editing Unison code, and interacting with the UCM any relative names not locally bound in your file will be resolved by prefixing them with the current namespace of
.tour.Your UCM commands and code are "scoped" to this namespace unless otherwise indicated with an absolute path.
Next, create a copy of the
baselibrary into a namespace called
.tour> fork .base lib.base
Earlier, when we used the
findcommand, we were searching from the root of our codebase, but within the
tournamespace, which might grow to contain multiple libraries, we'll want a different set of conventions for finding our own code versus searching through our dependencies.
Try using the
findcommand again for
.tour> find List.reverse I couldn't find matches in this namespace, searching in 'lib'... 1. lib.base.data.List.reverse : [a] -> [a]
When we'reinside the tour namespace,the UCM
findcommand will search for non-dependency Unison code in the current namespace tree before searching through the lib directory.
If you want to perform a search which specifically includes the
libnamespace, you can use the
.tour> find.all List.reverse 1. lib.base.data.List.reverse : [a] -> [a]
Notice how these results don't contain the top level
baseresult? That's because
find.allare both scoped to the
tournamespace based on where we're searching!
If we want a global search of our codebase, we can use the
Ok, that's enough preamble, let's start writing some Unison code!
Unison's interactive scratch files
The codebase manager lets you make changes to your codebase and explore the definitions it contains, but it also listens for changes to any file ending in
.uin the current directory. When any such file is saved (which we call a "scratch file"), Unison parses and typechecks that file. Let's try this out.
ucmterminal running and open up a file,
foo.u,or whatever you like) in your preferred text editor (if you want syntax highlighting for Unison files,follow this linkfor instructions on setting up your editor).
Now put the following in your scratch file:
use base square : Nat -> Nat square x = use Nat * x * x
This defines a function called
square.It takes an argument called
xand it returns
xmultiplied by itself.
The first line,
use base,tells Unison that you want to use short names for the base libraries in this file (which allows you to say
Natinstead of having to say
base.Nat).The UCM will prefer the
baseinstance found in
When you save the file, Unison replies:
✅ I found and typechecked these definitions in ~/unisoncode/scratch.u. If you do an `add` or `update` , here's how your codebase would change: ⍟ These new definitions are ok to `add`: square : base.Nat -> base.Nat Now evaluating any watch expressions (lines starting with `>`)… Ctrl+C cancels.
It typechecked the
squarefunction and inferred that it takes a natural number and returns a natural number, so it has the type
Nat -> Nat.It also tells us that
squareis "ok to `add`." We'll do that shortly, but first, let's try calling our function right in the
scratch.ufile, just by starting a line with
use base square : Nat -> Nat square x = x * x > square 4
And Unison replies:
6 | > square 4 ⧩ 16
6 |is the line number from the file. The
> square 4on line 6 of the file, starting with a
>is called a "watch expression", and Unison uses these watch expressions instead of having a separate read-eval-print-loop (REPL). The code you are editing can be run interactively, right in the same spot as you are doing the editing, with a full text editor at your disposal, with the same definitions all in scope, without needing to switch to a separate tool.
use baseis awildcard use clause.This lets us use anything from the
basenamespace under the
libnamespace unqualified. For example we refer to
Question:do we really want to reevaluate all watch expressions on every file save? What if they're expensive? Luckily, Unison keeps a cache of results for expressions it evaluates, keyed by the hash of the expression, and you can clear this cache at any time without ill effects. If a result for a hash is in the cache, Unison returns that instead of evaluating the expression again. So you can think of and use your
.uscratch files a bit like spreadsheets, which only recompute the minimal amount when dependencies change.
Let's try out a few more examples:
-- A comment, ignored by Unison > List.reverse [1,2,3,4] > 4 + 6 > 5.0 / 2.0 > not true
✅ ~/unisoncode/scratch.u changed. Now evaluating any watch expressions (lines starting with `>`)… Ctrl+C cancels. 6 | > List.reverse [1,2,3,4] ⧩ [4, 3, 2, 1] 7 | > 4 + 6 ⧩ 10 8 | > 5.0 / 2.0 ⧩ 2.5 9 | > not true ⧩ false
Testing your code
Let's add a test for our
square : Nat -> Nat square x = x * x test> square.tests.ex1 = use Nat == check (square 4 == 16)
Save the file, and Unison comes back with:
8 | test> square.tests.ex1 = check (square 4 == 16) ✅ Passed : Proved.
Some syntax notes:
test>prefix tells Unison that what follows is a test watch expression. Note that we're also giving a name to this expression,
- Note: there's nothing special about the name
square.tests.ex1;we could call those bindings anything we wanted. Here we use the convention that tests for a definition
- Note: these terms are being written while the UCM prompt is still reading
.tour>,so adding or updating them would mean that the fully qualified name for, say,
test.checkfunction has the signature
test.check : Boolean -> [test.Result].It takes a
Booleanexpression and gives back a list of test results, of type
view test.Result).In this case there was only one result, and it was a passed test.
A property-based test
Let's test this a bit more thoroughly.
squareshould have the property that
square a * square b == square (a * b)for all choices of
b.The testing library supports writing property-based tests like this. There's some new syntax here, explained afterwards:
use base square : Nat -> Nat square x = x * x use test test> square.tests.ex1 = check (square 4 == 16) test> square.tests.prop1 = go _ = a = !gen.natInOrder b = !gen.natInOrder expect (square a * square b == square (a * b)) runs 100 go
11 | go _ = a = !natInOrder ✅ Passed : Passed 100 tests.
This will test our function with a bunch of different inputs.
- The Unison block, which begins after an
=,can have any number ofbindings(like
a = …)all at the same indentation level, terminated by a single expression (here
expect (square ..)),which is the result of the block.
- You can call a function parameter
_if you just plan to ignore it. Here,
goignores its argument; its purpose is just to make
golazily evaluatedso it can be run multiple times by the
natInOrderis adelayed computation.A delayed computation is one in which the result is not computed right away. The signature for a delayed computation can be thought of as a function with no arguments, returning the eventual result:
() -> a.The
!gen.natInOrderevaluates the delayed computation, summoning a value of type
test.gen.natInOrder.It's a "generator" of natural numbers.
!natInOrdergenerates one of these numbers starting at 0 and incrementing by one each time it is called.
Adding code to the codebase
squarefunction and the tests we've written for it are not yet part of the codebase. So far they only exist in our scratch file. Let's add them now. Switch to the Unison console and type
add.You should get something like:
.tour> add ⍟ I've added these definitions: square : Nat -> Nat square.tests.ex1 : [Result] square.tests.prop1 : [Result]
You've just added a new function and some tests to your Unison codebase under the
tournamespace. Try typing
view square.tests.prop1.Notice that Unison inserts precise
usestatements when rendering your code.
usestatements aren't part of your code once it's in the codebase. When rendering code, a minimal set of
usestatements is inserted automatically by the code printer, so you don't have to be precise with your
If you type
testat the Unison prompt, it will "run" your test suite:
.tour> test Cached test results (`help testcache` to learn more) ◉ square.tests.ex1 : Proved. ◉ square.tests.prop1 : Passed 100 tests. ✅ 2 test(s) passing Tip: Use view square.tests.ex1 to view the source of a test.
But actually, it didn't need to run anything! All the tests had been run previously and cached according to their Unison hash. In a purely functional language like Unison, tests like these are deterministic and can be cached and never run again. No more running the same tests over and over again!
Moving and renaming terms
When we added
square,we were at the
squareand its tests are at
tour.square.We can also move the terms and namespaces to different locations in our codebase with the
.tour> move.term .tour.square mySquare Done. .tour> find 1. mySquare : base.Nat -> base.Nat .tour> move.namespace square.tests mySquare.tests Done.
.tour.squareto refer to the
squaredefinition with an absolute path, and then moving it to therelativename
mySquare.When you're done shuffling some things around, you can use
findwith no arguments to view all the definitions under the current namespace:
.tour> find 1. mySquare : Nat -> Nat 2. mySquare.tests.ex1 : [Result] 3. mySquare.tests.prop1 : [Result]
Also notice that we don't need to rerun our tests after this reshuffling. The tests are still cached:
.tour> test Cached test results (`help testcache` to learn more) ◉ mySquare.tests.ex1 : Proved. ◉ mySquare.tests.prop1 : Passed 100 tests. ✅ 2 test(s) passing Tip: Use view square.tests.ex1 to view the source of a test.
We get this for free because the test cache is keyed by the hash of the test, not by what the test is called.
When you're starting out writing some code, it can be nice to just put it in a temporary namespace, perhaps called
scratch.Later, without breaking anything, you can move that namespace or bits and pieces of it elsewhere, using the
Modifying existing definitions
Instead of starting a function from scratch, often you just want to slightly modify something that already exists. Here we'll make a change to the implementation of our
edit mySquarefrom your prompt (note you can use tab completion):
.tour> edit mySquare ☝️ I added these definitions to the top of ~/unisoncode/scratch.u mySquare : Nat -> Nat mySquare x = use Nat * x * x You can edit them there, then do `update` to replace the definitions currently in this branch.
This copies the pretty-printed definition of
mySquareinto your scratch file "above the fold." That is, it adds a line starting with
---and puts whatever was already in the file below this line. Unison ignores any file contents below the fold.
mySquareand instead define
mySquare x(just for fun) as the sum of the first
xodd numbers (here's anice geometric illustration of why this gives the same results):
use base mySquare : Nat -> Nat mySquare x = sum (map (x -> x * 2 + 1) (range 0 x)) sum : [Nat] -> Nat sum = foldLeft (+) 0
✅ I found and typechecked these definitions in ~/unisoncode/scratch.u. If you do an ''add'' or ''update'' , here's how your codebase would change: ⍟ These new definitions are ok to `add`: sum : [Nat] -> Nat ⍟ These names already exist. You can `update` them to your new definition: mySquare : Nat -> Nat
Adding an updated definition to the codebase
Notice the message says that
mySquareis ok to
update.Let's try that:
.tour> update ⍟ I've added these definitions: sum : [Nat] -> Nat ⍟ I've updated these names to your new definition: mySquare : Nat -> Nat
Only affected tests are rerun on
If we rerun the tests, the tests won't be cached this time, since one of their dependencies has actually changed:
.tour> test ✅ New test results: ◉ mySquare.tests.ex1 : Proved. ◉ mySquare.tests.prop1 : Passed 100 tests. ✅ 2 test(s) passing Tip: Use view mySquare.tests.ex1 to view the source of a test.
Notice the message indicates that the tests weren't cached. If we do
testagain, we'll get the newly cached results.
The dependency tracking for determining whether a test needs rerunning is 100% accurate and is tracked at the level of individual definitions. You'll only rerun a test if one of the individual definitions it depends on has changed.
Publishing code and installing Unison libraries
Code is published to Unison's own code hosting solution,Unison Share,using the
pushcommand and libraries are installed via the
pullcommand. There's no separate tooling needed for managing dependencies or publishing code, and you'll never encounter dependency conflicts in Unison.
In Unison, a namespace is a mapping of names to their underlying definitions.
We often think of these names as forming a tree, much like a directory of files, and names are like file paths in this tree.
.> add myNewTerm
Adds all the definitions from the most recently typechecked file to the codebase.
FAQ about theaddcommand
I got an error about "these definitions failed" on add
This message happens when some of the definitions couldn't be added to the codebase. UCM shows a table of definitions along with the reason why they didn't succeed, like this:
x These definitions failed: Reason needs update myFunction : Doc
Here's what these reasons mean:
- needs update:The scratch file has a definition with the same name as an existing definition. Doing
updateinstead of add will turn this failure into a successful update.
- conflicted:The file has a definition whose name is currently conflicted. Resolving the conflict and then trying an update again will turn this into a successful update.
- term/ctor collision:A definition with the same name as an existing constructor for some data type. Rename your definition or the data type before trying again to
- ctor/term collision:A type defined in the file has a constructor that's named the same as an existing term. Rename that term or your constructor before trying again to
- needs alias:A definition in the file already has another name. You can use the
alias.typecommands to create new names for existing definitions.
- blocked:This definition couldn't be added because it dependended on another definition in the file that had a failed status.
- extra dependency:This definition was added because it was a dependency of a definition explicitly selected.
I want to undo a partially completed add where some of the definitions failed
You can use the
undocommand in the case of an undesired partially completed add.
The use clause allows code in the currentlexical scopeto use terms and types in the given namespace without referencing them by their fully qualified names.
use Nat drop + (drop 5 4) + 8
usecan be called without referencing specific terms--''use List'' brings everything under the
Listnamespace into scope, or it can reference specific definitions, as in,
use List map flatMap