Common workflows for updating Unison code

The process of applying a potentially breaking change to your code need not be stressful. Here's how Unison handles that process in a few common cases:

Updating your own code

Sometimes you'll need to make a change in the codebase that can't automatically be propagated to dependent functions, for example: if you alter a data constructor for a type or change the arguments to a function. Unison can programmatically guide you through the edits that you need to make in these cases.

Suggested workflow summary:

  1. enter update in the UCM to save your changes
  2. The UCM opens up non-typechecking code in your editor
  3. Fix the impacted terms and save the file
  4. run update to commit your changes.

Walk through an example

Let's say we have a simple type and a few functions which make use of that type in our codebase:

unique type Box = Box Nat

Box.toText : Box -> Text
Box.toText box = match box with
  Box.Box nat -> Nat.toText nat

Box.print : Box -> {IO,Exception} ()
Box.print box =
  Box.toText box |> printLine
myProject/main> add

We add it to the codebase but later, we may realize its data constructor should be changed.

myProject/main> edit Box
unique type Box = Box Int

We've changed the data constructor from taking a value of type Nat to Int. Upon saving the file and running update again, the UCM will open up the impacted terms in your editor.

myProject/main> update

Okay, I'm searching the branch for code that needs to be
updated...


That's done. Now I'm making sure everything typechecks...


Typechecking failed. I've updated your scratch file with the
definitions that need fixing. Once the file is compiling, try
`update` again.

All impacted terms, including indirect dependents, will be opened in your scratch file.

Box.print : Box ->{IO, Exception} ()
Box.print box = Box.toText box |> printLine

Box.toText : Box -> Text
Box.toText = cases Box nat -> Nat.toText nat

unique type Box = Box Int
The 1st argument to `Nat.toText`

        has type:  Int
  but I expected:  Nat

  5 | Box.toText = cases Box nat -> Nat.toText nat
  6 |
  7 | unique type Box = Box Int

The UCM will start printing typechecking errors to the console starting from the top of the file so you know what to tackle first. In our example, the function Box.print is opened in the editor even though the change we need to make is in the Box.toText function. That's because resolving larger, more complicated updates can involve propagating changes to many terms, all the way up the function call chain. With this workflow, you can be assured that your change will leave your codebase in a consistent state. Once all the errors are fixed, you can run update again to commit your changes.

How to update a library dependency

Upgrading a library is very similar to the regular process of updating Unison code. It involves one additional simple command. The following workflow uses Unison's standard library, base, as an example.

Upgrade workflow:
    • lib.install the latest version of the dependency into your codebase so that it is a sibling of your current library version.
      • myProject/main> lib.install @unison/base
    • Run the upgrade command, indicating which library you'd like to upgrade
      • myProject/main> upgrade unison_base_1_0_0 unison_base_2_0_0
  1. The UCM will create a new branch for the upgrade to take place in, so if something goes wrong and you want to back out of your changes, you can easily switch back to the branch you were on before the upgrade.
  2. If there are conflicts to resolve, the UCM will open up the affected terms in your editor. Resolve the conflicts and enter update again once the file typechecks.
  3. Run upgrade.commit to merge the temporary branch created by the upgrade command back into its parent branch. It will delete the temporary branch.
You do not need to remember the exact versions of the libraries you are upgrading from and to. Enter upgrade with no arguments to pick from the list of libraries in your project.

Updates from merging

Merging two branches together can also prompt an update workflow. The merge commands below are all valid ways to merge changes from one branch to another.

scratch/main> merge /featureBranch
scratch/main> merge /featureBranch /featureBranch2
scratch/main> merge /@contributor/featureBranch

If a term has been updated on the parent branch and the child branch being merged into it the UCM will do two things:

  1. It will create a branch with the name merge-child-into-parent for the merge to take place in.
  2. It will open up the impacted terms in a scratch file. Conflicting terms are prefixed by their branch name in a comment for resolution.
-- tmp/main
term1 : base.Nat
term1 =
  use base.Nat +
  1 + 2 + 4000

-- tmp/updateTerm1
term1 : base.Nat
term1 =
  use base.Nat +
  1 + 2 + 5000

Delete the term that is no longer up-to-date or combine the changes into one term, then run update to apply the change to the merge resolution branch.

tmp/merge-updateTerm1-into-main> update

Once the codebase changes are resolved, run merge.commit to commit the changes into the parent branch. merge.commit will also delete the merge resolution branch.

tmp/merge-updateTerm1-into-main> merge.commit