# What is so unique about Unison?

I have recently started exploring the [Unison programming language](https://www.unison-lang.org). Some programming languages are good because they improve the current state of affairs. For example [Gleam](https://gleam.run) brings type-safety to the Erlang platform. Other languages are *game-changers*:

* When [Scala](https://www.scala-lang.org/) was introduced, it brought an incredible amount of (statically-typed) functional programming features to the JVM. This changed the way we approached mutability, error management, concurrency, and structured our programs.
    
* [Verse unifies functional-programming and logical programming](https://www.youtube.com/watch?v=OJv8rFap0Nw), where notions of failure, multiple-valued variables, and transactions, are baked into the language. This changes the way we think about control flow.
    
* [Unison](https://www.unison-lang.org/) is a *frictionless language*. It builds on the idea of "immutable code" to give us a huge productivity boost for writing cloud applications.
    

In this post, I am sharing my first steps with Unison. The parts where I was delighted but also the parts where I was confused. I will only talk about my experience of developing a small library for now. Part 2 will be about developing a full cloud application for my own needs.

# First steps

I didn't need much to get started with Unison:

* An executable called the `Unison Code Manager` (`ucm` for short). It is available as [a nix package](https://search.nixos.org/packages?channel=23.11&show=unison-ucm), so it was very easy for me to [install it with `flox`](https://etorreborre.blog/why-you-should-flox-every-day).
    
* VS Code with [the Unison plugin](https://marketplace.visualstudio.com/items?itemName=unison-lang.unison).
    

I then created a `quickstart` project with `project.create quickstart` using `ucm`. With this project, I was all setup for coding. Here is a simple `hello` function:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1714735559651/c5211234-784a-4573-92a9-60b5998ec982.png align="center")

The first unusual thing with unison is that *files don't matter*. A convenient setup for your editor is a split screen as above with *just one file* on the left, and `ucm` on the right. Where is my code then? It is in a database:

```bash
ll $HOME/.unison/v2
Permissions Size User        Date Modified Name
.rw-r--r--@    0 etorreborre 14 Apr 12:48  unison.lockfile
.rw-r--r--@  13M etorreborre 14 Apr 12:55  unison.sqlite3
```

Then, I went through the [Unison language tour](https://www.unison-lang.org/docs/tour/) and learned how to:

* Search for functions: by name, but also by type!
    
* Create new functions and add them to my codebase.
    
* Test my code by either running it or writing tests.
    

Not impressed? Let's have a closer look!

### Search by type

Haskell users know the benefits of a tool like [Hoogle](https://hoogle.haskell.org/). With such a tool, you can search functions by type. This is soooooo convenient! "*How do I serialize* `Text` *to* `Bytes`?". **Just type** `find : Text -> Bytes` and `ucm` returns all the functions with that signature:

```plaintext
  1. lib.base.Text.toUtf8 : Text -> Bytes
  2. lib.base.IO.net.URI.Path.encode.segment : Text -> Bytes
```

This even works when there are type parameters (Hoogle is better though because it will also show you functions where the order of parameters is slightly different).

### Add a function to the project

Once a function typechecks, you can `add` it to your project, or `update` it, if was added before. This has profound implications:

* **All the code added to your project *always* compiles**. If an updated definition conflicts with the previous version, then all the conflicting code is brought up to be fixed.
    
* Adding a function to your project **is also the equivalent of committing it**. No more `git add .; git commit -m "add some code"`. Later on, sharing the code with others is just a matter of running `push`.
    
* **Codebase changes can be reversed** by using the `reflog`, as you would do with git.
    
* The project knows about functions, not just text. This means that:
    
    * You can easily **rename a function**.
        
    * **You can give several aliases to the same function**. Internally, a function is identified by the hash of its contents.
        
    * You can create patches that are selectively applied to part of the code, **so that the result still typechecks** (I haven't played too much with that though).
        

### Run and test my code

Veteran LISP programmers know the value of having a [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) at their fingertips. With Unison, it is as simple as writing a "watch expression", with `>`:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1714738595370/5be79f94-d6c2-44bf-b4be-098f4b6c42ba.png align="center")

That expression is automatically evaluated in `ucm` so you get immediate feedback on your code. Writing a test is no different, except that you give it a name, and use the `test` library functions:

```haskell
test> hi.test = verify do
  ensureEqual (hi "Eric") "Hi, Eric!"
```

This test is just another term that can be added to the code base to grow the test suite. If it passes of course! Two things to note:

* When you change code in your editor, all the tests are re-executed. If those tests don't depend on some changed code, t**heir result is cached, they are not re-executed**!
    
* This also means that you can break previously added tests, which are present in your editor anymore, when updating a function. In that case, you can call `test` in `ucm` to re-run all the tests.
    

## Some feedback from my first steps

I don't want to give the impression that everything is perfect. The language tour and first experience could be improved:

* In `ucm,` typing `fn + ←` does not go to the beginning of the line as it does in my `zsh` terminal.
    
* Hovering over a term shows its definition. It would be great to also be able to:
    
    * Show the doc.
        
    * Show the code.
        
    * Open the term in the editor to modify it.
        
* This would reduce the amount of back and forth switching between the editor and `ucm`.
    
* Selecting a term to edit it is a bit crude. The term to edit is added at the top of the current file and the rest of the file is commented out. In many cases, I want my existing code to stay in scope.
    
* Some crucial aspects of the platform are still being polished! While I was going on the language tour, the Unison team realized that the functions used to do property-based testing were not so great and [revamped the whole thing](https://share.unison-lang.org/@unison/base/releases/3.0.0). This means that some of the documentation or blog post you read about Unison might not always reflect the latest and greatest.
    
* Similarly, the concept of namespacing for terms (the `name1.name2.term` notation) is a bit in flux. There are still [a number of issues](https://github.com/unisonweb/unison/issues?q=is%3Aissue+is%3Aopen+cd+) regarding the interactions of `ucm` commands for navigating/updating the code: `cd`, `up`, `ls`, `move`,... and the qualifiers that need to be written or not in the edited code.
    
* The VS Code integration with `ucm` could be improved. Some specific windows would be nice to display:
    
    * All the compilation errors (and being able to jump to one of them)
        
    * All the tests (in the current file, in the whole project)
        
    * All the watch expressions (in the current file, in the whole project)
        
* I think I had a few bugs with the editor:
    
    * Sometimes `ucm` is happy with a function, yet the editor shows an error message.
        
    * If you define a `==` function, with its documentation, add it, and try to edit the documentation again, then the editor shows an "offset" error for `(Name.==.doc)`.
        
* Since we pay less attention to files, it's easy to delete terms that were not yet added, i.e. losing that code! In that case having an editor with a good [local history is important](https://marketplace.visualstudio.com/items?itemName=xyz.local-history).
    

# My first library

I want to use Unison to develop a cloud application but I think that it is important to get comfortable first with a new programming language and the full lifecycle of: coding, testing, releasing. So I decided to implement a library that did not exist yet: [@etorreborre/multimap](https://share.unison-lang.org/@etorreborre/multimap).

This library provides a data type, a `MultiMap`. This is simply a wrapper for a `Map k vs` where `vs` is a list of values. `MultiMap`s can be useful in a variety of contexts. For example, in Unison we could have a `MultiMap` tracking all the existing names for a given term hash.

That led me to appreciate a few more cool things about Unison.

### Documentation is first class

Yes, in Unison, the documentation for a term is itself a term. It has a name, a namespace, can be added and edited. It can also be executed! This means that you can put both Markdown text in a block of documentation and some valid Unison code:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1714810807780/4a0aaa19-7134-4de4-bd0b-bf2d3c2f14b0.png align="center")

Then, the `docs` command executes and displays the result on the right. And eventually that documentation is published as some beautiful HTML:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1714811172470/e31df60b-13a0-43ae-ab54-6c8062d389b2.png align="center")

Why is this important? This matters a lot because now we have a much greater incentive to write great docs:

* They always typecheck.
    
* They are refactored like the rest of the code when a name is updated.
    
* Links to other terms are always working.
    
* You can provide great examples inline.
    

## Less attention to files = more focus

When writing Unison code we don't jump from file to file. We keep editing code in the same window. This means that it's perfectly ok to clear everything and just focus on one or two functions at the time, with the corresponding tests. This is strangely liberating.

## Less attention to files = more normalization

Many people came to the idea that a good syntax formatter was the best way to put formatting discussions to rest. For example, `Go` came up with a default formatter from the start, and in Haskell, a "no-configuration" formatter like [ormolu](https://github.com/tweag/ormolu), won the hearts of many teams.

Unison goes one step further since no text is really stored, only an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree). This means that you don't really care if you prefix all your functions with their namespace, or add a bunch of `use` clause at the beginning of the file. Eventually the code will be stored and rendered in the same way. For example:

```haskell
use MultiMap empty size isEmpty
test> MultiMap.tests.ex1 = verify do
    size empty === 0
    isEmpty empty === true
```

is reformatted as:

```haskell
MultiMap.tests.ex1 : [Result]
MultiMap.tests.ex1 = test.verify do
  use MultiMap empty
  MultiMap.size empty === 0
  MultiMap.isEmpty empty === true
```

## Todo

This is a small but useful thing when you use types to design your code. For example, you know that you will need a function with a given type but you don't want to lose focus by implementing it right away. Just leave `todo` as its implementation. This is always a valid term!

## Testing

Testing is great! Unison comes with an effect system, called [abilities](https://www.unison-lang.org/docs/fundamentals/abilities/), where a piece of code can interact with a handler to decide how the computation should be executed. This possibly sound nebulous, but you probably have already used something like this if you have:

* Thrown exceptions. Then, a handler knows what to do with the exception. "Resumable exceptions" are even better since the handler can provide a value to continue the computation.
    
* Used `async` in Rust. The handler is an executor that puts the current code on hold, then resumes it when a thread is available.
    
* Used a generator in Python with `yield`. In that case, the handler uses the yielded value then resumes the code, to get the next value.
    

The abilities used for testing in Unison are:

* `Random` to provide randomly generated values.
    
* `Each` to repeat a piece of code, possibly with a different value, taken from a list of values.
    
* `Exception` to abort the test in case of a failure.
    

This means that you have a very convenient way to mix [*enumeration testing*](https://github.com/rudymatela/leancheck#readme), where data is tested exhaustively, and [*random testing*](https://en.wikipedia.org/wiki/Random_testing), where you let randomness explore the test data. You can read some examples on Paul Chiusano's [blog post on the subject](https://pchiusano.blog/posts/mixing-random-and-exhaustive-testing).

## Publishing the library

Publishing a library is very easy:

* Provide a README, in the form of a documentation term called,... `README`.
    
* Use [the `License` datatype](https://share.unison-lang.org/@unison/base/code/releases/3.2.0/latest/types/@7k7de5eej4eflq5h09kr5jno5dmdkepdhpd25r71nbh9a0cdpiisltoj8en3pe47ifgscuuvd19n9o1vluifeorj7gosn7b33m3vp5g) to provide a license for your project (and [`mit` as a `LicenseType`](https://share.unison-lang.org/@unison/base/code/releases/3.2.0/latest/terms/@t1oajjddc10la9dlhlrp95m3th4ag3oj9tktdl4vokd1f9ljcq1969p37goqmlont7l2bjqdjusd0qqn85kc6oqpi4oakbn5q0kf29o) for example).
    
* `push` and press the `Cut a new release` button [on the project page](https://share.unison-lang.org/@etorreborre/multimap/releases).
    

## Some question marks

The `MultiMap` library is not so small. It contains more than 200 terms and 2000 lines of code (counting the documentation. It's code, right 😊?).

With this library I could get a taste of what it meant to work with larger projects in Unison and ask myself a few questions.

### Namespaces

Namespaces are mostly irrelevant for a library like `MultiMap` since everything is at the same level. But where do you put tests? Possibly, we want to separate the testing code from the "production" code. I opted for the `MultiMap.tests` namespace (Unison's base library does things a bit differently). This has the advantage of offering a clear separation, and if you want to have a look at all the tests you can `edit.namespace MultiMap.tests`.

There is also a bit of "convention over configuration". The `lib` namespace in a given project is treated specially. It contains all the project dependencies, including [Unison's base library](https://share.unison-lang.org/@unison/base). If you want to upgrade that library or add another library as a dependency you need to `pull` it, inside the `lib` namespace:

```haskell
pull @unison/json-core/releases/1.0.3 lib.jsonCore_1_0_3
```

As you can see, there are no real constraints for going from `json-core/releases/1.0.3` to `lib.jsonCore_1_0_3`. I think I would have preferred a `pull dependency @unison/json-core/releases/1.0.3` command doing the right thing for me.

I also made the mistake of pulling the *full* Unison base library directly into my top-level namespace 😰. Then I *really* had to learn how to remove terms and namespaces 😁.

### Privacy

Closely related to namespaces is privacy. There's no notion of public/private in Unison and no notion of modules. Only namespaces. In `MultiMap` there are only 2 functions that are implementation functions. I decided to "hide" them in [a `private` namespace](https://share.unison-lang.org/@etorreborre/multimap/code/releases/0.0.1/latest/namespaces/MultiMap/private).

I think that having the ability to truly forbid some terms to be seen outside of their packages would help when scaling Unison programs to large programs.

### No typeclasses

I like to call [typeclasses](https://typelevel.org/cats/typeclasses.html) "interfaces for data types", and as such, I think they play an important role for modularity. They also come with all sort of questions:

* How do you pass typeclass instances where needed?
    
* How do create/resolve typeclass instances?
    

Unison does *not* have typeclasses, and I can understand why. While they are incredibly convenient, they also make a programming language [a lot more complex](https://github.com/fsharp/fslang-suggestions/issues/243#issuecomment-916079347). OCaml, or Elm, for example, are two functional programming languages with no support for typeclasses.

As a matter of convenience, Unison has universal equality and universal ordering, which are both *structural*. What does that mean?

This means that when you define any data type, for example `Person`, equality and ordering are based on what is *inside* the data type:

```haskell
type Person = {
  name: Text,
  age: Nat,
}
```

The `Universal.eq` and the `Universal.gt` functions compare 2 `Persons` first based on their `name`, *then* on their `age`. This even works with functions! Two functions are equal if the hash of their content is equal.

Structural equality is not controversial, but structural ordering is more questionable:

* We might want to define different orderings for the same data type.
    
* I'd rather avoid breaking code if I simply swap two fields in my data type.
    

As a result, some Unison APIs use a comparison function parameter to specify exactly how elements must be compared. But this is not the case for some data types like `Map` or `Set`.

### Large refactorings

While developing the `MultiMap` library, I inadvertently set myself up for a large refactoring. I initially started with `MultiMap = Map k [v]` and later, much later, realized that a more correct definition was `MultiMap = Map k (List.Nonempty v)`.

Changing the datatype, and trying to `add` the new definition, triggered the typechecker, which was very unhappy with this change. `ucm` brought most of the definitions into scope and told me to fix them. This has been unfortunately a bit painful experience:

* Only some errors are displayed at the time. Fixing one issue triggers another issue in a different place and you don't really know when the whole process is going to stop.
    
* Some names were replaced with their hash.
    
* After fixing all issues in sight, I entered `update` but `ucm` told me that some code was still not compiling, commented out all the previous code, only to put another wall of text in front of me. Was it some new code, a bit of the old code that was still not compiling, some additional tests? Eventually, I had the *impression* of writing some fixes twice.
    

That made me wonder if more tooling was needed for large codebases, in order to support deep refactorings.

# My first contributions

During the development of the `MultiMap` library, [I added a few functions that were missing from the `List` and `List.Nonempty` data types](https://share.unison-lang.org/@etorreborre/p/contributions). The whole process was really simple:

* Create a branch in @unison/base: `branche.create /mybranch`.
    
* Add some new functions + doc + tests.
    
* `push`.
    
* [Open a contribution](https://share.unison-lang.org/@unison/base/contributions) in the corresponding project (much like a pull request in Github).
    

All of this, except the last step, is done in `ucm` and I can foresee a future where even the last step is supported in `ucm` so that you don't even have to leave your development environment to collaborate on projects.

# Conclusion

Despite some of the shortcomings mentioned above, I can't wait to develop my first cloud application. This is going to be an application for importing bank data into my accounting software. My prediction is that most of the difficulty will come from the core of the application:

* Dealing with authentication on both systems.
    
* Navigating both data models.
    
* Determining what is "new" data and what is already imported data.
    

I hope that, on the contrary, thanks to [Unison Cloud](https://www.unison.cloud), the technical aspects will be a breeze:

* Deploying the application.
    
* Monitoring it.
    
* Using secrets.
    
* Interacting with HTTP APIs.
    
* Maybe storing a bit of data.
    

Stay tuned for part 2, and don't hesitate to give Unison a go, it is really a refreshing experience!
