Unison: From 0 to Cloud
Tags: unison, functional, distributedsystems, programming
Published: 2023-12-20 16:15:40

📹 Hate reading articles? Check out [the complementary video](https://youtu.be/F6S7NHnK1vA), which covers the same content.

---

![In 2 minutes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6alzrpozh2pplx51yhfc.PNG)

Let’s cut to the chase. Let’s deploy something. Currently, I have no project. I have Unison language [installed](https://www.unison-lang.org/docs/quickstart/) (which I brew installed on a mac) and a Unison account (I one-clicked signed-in with GitHub).

> 💡 We can use a Unison account with Unison Share and Unison Cloud.
> 

First, we start Unison Codebase Manager by running `ucm`.

> 💡 The UCM is the interface to the Unison codebase
> 

Then we type `projects` to list all the projects and see nothing. We can create a new project with `project.create-empty`:

```bash
.> projects                 
.> project.create-empty

  🎉 I've created the project with the randomly-chosen name ambitious-porcupine 

  ...
```

Instead of constructing a whole project by hand, we can use some templates. We fetch a few simple examples and required dependencies with `pull @unison/cloud-start/releases/latest`:

```bash
ambitious-porcupine/main> pull @unison/cloud-start/releases/latest
  ... 

  ✅ Successfully pulled into ambitious-porcupine/main, which was empty.
```

Right after, we can deploy a hello-world project:

```bash
ambitious-porcupine/main> run examples.helloWorld.deploy

Service exposed successfully at:
  https://zelenya.unison-services.cloud/h/wqcwa3uyfsxqrle3ut7fhbfno4qkkqdy2flspsnbqsfeb62m5cgq/
Service with hash: wqcwa3uyfsxqrle3ut7fhbfno4qkkqdy2flspsnbqsfeb62m5cgq now available at: 
  https://zelenya.unison-services.cloud/s/hello-world/
...
```

When we open one of these urls with a name as a path parameter, for example, https://zelenya.unison-services.cloud/s/hello-world/Joseph, we see a greeting:

```haskell
👋 hello Joseph
```

For those who aren’t convinced or those who want to see the hello capitalized and with a comma, we can edit **the logic** really quickly:

```
ambitious-porcupine/main> edit examples.helloWorld.logic

I added these definitions to the top of .../scratch.u

...

You can edit them there, then do `update` to replace the definitions currently in this namespace.
```

Let’s jump over these details for now and just modify the logic in the `scratch.u` file, named this way because it’s meant to be thrown away. 

The service's logic is a function that accepts an `HttpRequest` and returns an `HttpResponse`. 

```haskell
helloWorld.logic : HttpRequest ->{Exception, Log} HttpResponse
helloWorld.logic = Route.run do
  use Text ++
  name = route GET Parser.text
  info "request for greeting" [("name", name)]
  ok.text ("👋 hello " ++ name ++ "\n")
```

To modify this request body, we change the last line:

```haskell
helloWorld.logic : HttpRequest ->{Exception, Log} HttpResponse
helloWorld.logic = Route.run do
  use Text ++
  name = route GET Parser.text
  info "request for greeting" [("name", name)]
  ok.text ("👋 Hello, " ++ name ++ " 👋 \n")            -- HERE
```

Because `ucm` is listening for changes in the current file, when we save it, Unison parses and typechecks it:

```
ambitious-porcupine/main> 

  I found and typechecked these definitions in .../scratch.u. If you do an `add` or
  `update`, here's how your codebase would change:
  
    ⍟ These names already exist. You can `update` them to your new definition:
    
      examples.helloWorld.logic : HttpRequest ->{Exception, cloud_6_0_6.Log} HttpResponse
```

We can update the definition (the hello world’s logic) using `update` as `ucm` suggests:

```
ambitious-porcupine/main> update

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

  ...

  Done.
```

If we redeploy again and check the response, we get an updated text:

```
ambitious-porcupine/main> run examples.helloWorld.deploy

Service exposed successfully at:
  https://zelenya.unison-services.cloud/h/yt4xnmaxvjlg4enlmatsv57d5u6hffar74pfty4uephgav42eaxq/
Service with hash: yt4xnmaxvjlg4enlmatsv57d5u6hffar74pfty4uephgav42eaxq now available at: 
  https://zelenya.unison-services.cloud/s/hello-world/
```

[https://zelenya.unison-services.cloud/s/hello-world/Joseph](https://zelenya.unison-services.cloud/s/hello-world/Joseph):

```haskell
👋 Hello, Joseph 👋
```

Okay, we saw that the service’s logic is a regular function. But what about the deployment? *Clearly, all the complexity hides there.*

Hold on to your socks, folks! Deploy is just a function too:

```
ambitious-porcupine/main> view examples.helloWorld.deploy
```

```haskell
examples.helloWorld.deploy : '{IO, Exception} ()
examples.helloWorld.deploy = Cloud.main do
  name = ServiceName.create "hello-world"
  serviceHash = deployHttp !Environment.default helloWorld.logic
  ignore (ServiceName.assign name serviceHash)
  printLine "Logs available at:\n  https://app.unison.cloud"
```

This function deploys an http service with `helloWorld.logic` we just saw, while we just sit there and enjoy.

## Painless deployments?

Only functions? Yes! There are no YAML files, no packaging, no containers, or anything like that.

**You call a function to deploy a service!** Unison takes care of the dependencies, caching on the server, and so on. Deployment takes seconds.

### How is this even possible?

You might have noticed the unusual urls and plain hashes associated with services:

```bash
ambitious-porcupine/main> run examples.helloWorld.deploy
...
Service with hash: wqcwa3uyfsxqrle3ut7fhbfno4qkkqdy2flspsnbqsfeb62m5cgq now available at: 
...
```

```
ambitious-porcupine/main> run examples.helloWorld.deploy
...
Service with hash: yt4xnmaxvjlg4enlmatsv57d5u6hffar74pfty4uephgav42eaxq now available at:
... 
```

> 💡 The default service URLs are based on the service hash (see [ServiceHash](https://share.unison-lang.org/@unison/cloud/code/releases/6.1.0/latest/types/ServiceHash)).
> 

This hash is **a hash of the service implementation**! In other words, Unison takes the source code of the service with its metadata and makes a unique identifier out of it.

A hash identifies a service and a service is immutable. When we change the logic, we don't modify a service, instead, we deploy a new one, and we get a new hash based on the service’s implementation.

### Content-addressing

This concept, **content-addressing**, is a [cornerstone](https://www.unison-lang.org/docs/the-big-idea/) for Unison. And it applies to any piece of code. The Unison code is identified by its content and not by its location (like we would typically think about code). Let’s unroll this.

With a typical programming language, when you write some code, you modify some text on a specific line in a specific file.

This is not the case with Unison — Unison code doesn’t even live in a text-based file. We can still modify it using the text editor (as we’ve seen) but we have to pass this code to the tool such as `ucm` that does the actual change. Let’s look at an example. Given a function:

```haskell
increment : Nat -> Nat
increment n = n + 1
```

Unison parses it as a syntax tree:

```haskell
increment = (#arg1 -> #a8s6df921a8 #arg1 1)
```

And then hashes it and stores it in the database.

The human-readable names, such as `increment` and [`+`](https://share.unison-lang.org/@unison/website/code/main/latest/terms/@@Nat.+), is metadata that doesn’t affect the function’s hash and is stored separately.

---

Because it’s so important, let’s look at another example. Let’s try to define a new type that represents a box that optionally has a value (something known as `Option` or `Maybe` in other languages), which consists of two variants:

- `Emptiness`, which represents a lack of value.
- `Exists`, which wraps a value of any type.

```haskell
structural type MyBox a
  = Emptiness
  | Exists a
```

> 💡`structural` means that the types defined with the same structure (or shape) are identical.
> 

When we save the scratch file, the `ucm` tells us that this definition already exists and is also named `lib.base.Optional`! If we check the implementations or the hashes, we can see that we just have **different names** for **the same type**. It’s pretty fun.

*But it’s not just about fun.*

## Benefits

Having content-addressed code brings tons of other benefits and changes the way we work.

It starts with simple niceties such as **instant renames** (because it’s just a metadata change in a database, not a complicated semantic analysis that has to be done when you refactor a name with mainstream tooling).

It also means **no builds** and **no test reruns** (Unison needs to hash your function just once — if you don’t change its implementation it doesn’t need to redo anything, furthermore it knows that there is no need to rerun the tests), **no dependency hell** **nor version conflicts** (if you have two “different versions” of a function brought by dependencies it means that you have two different functions, and you can either unify the usage or keep using two functions), and so on.

It shines the most when it comes to **distributed programming**. Unison is designed with distributed computing in mind. We can move computations from one location to another, and the receiver either has all the hashes already and can perform the computation, or it has to request and sync the missing dependencies (which then will be cached).

Unison handles serialization and deserialization of data, which makes it easier to send data over the network. *Let’s talk about that in detail.*

### Painless RPCs

You can call other services and storages as smoothly as local functions (no need to convert the data from and to json, protobuf, or whatever).

Let’s just try it out. `examples.multiService.deploy` demonstrates a multi-service deployment and usage. This time, let’s view it in `ui` (or you can look at [Unison Share](https://share.unison-lang.org/@unison/cloud-start/code/releases/1.5.0/latest/terms/examples/multiService/deploy)).

We start by deploying two services: the first one takes a natural number and returns it incremented by one, and the second — decrements by one.

```haskell
h1 = deploy! default (x -> x + 1)
incrService = create "increment"
ignore (assign incrService h1)
```

We use the `deploy` function to deploy a service, which takes an environment along with a function of the shape `a ->{abilities} b` (don’t worry about these *abilities* for now) and returns a `ServiceHash a b`, where `a` is the input type of the service and `b` is the output type. 

Then we create the `edge` service, which is going to be a public-facing gateway service that speaks HTTP,  deals with the real world, and delegates to the two typed services we just saw. 

```haskell
edge : HttpRequest -> HttpResponse
edge =
  ...
  incr = do
    n = route GET (s "increment" / nat)
    n' = callName incrService n
    text (toText n')
  decr = do
    ...
  Route.run (incr <|> decr)
n = create "counter-edge-service"
ignore (assign n (deployHttp !default edge))
```

Note that we deploy this service with `deployHttp` (not generic `deploy`) because its input type is `HttpRequest` and output type is `HttpResponse`.

We use `callName` to call other Unison service (it takes a service name and an input):

```haskell
callName : ServiceName a b -> a ->{Services, Remote} b 
```

If we try to change the code to pass an argument of the wrong type (for example a string instead of a natural number), it won’t typecheck:

```haskell
The 2nd argument to `callName`
  
            has type:  Text
      but I expected:  Nat
  
     26 |         n' = callName incrService "string, not natural" 
```

We don’t have to deal with serialization code and we can never send the wrong blob of data to a service.

### Everything is a function

Let’s do a quick recap, in Unison world:

- Deployment is done with a function call.
- Calling another service is a function call.
- Accessing storage is a function call.
- A function call is a function call (just for completeness 😅).

> 💡 We’re not going to look at the example of using [typed durable storage](https://share.unison-lang.org/@unison/cloud/code/releases/6.1.0/latest/types/@7lcon9l2lb801i56bhidrasm8te9op95ivf09f57fod5qct4fp8jnhchm7lbir8882l1cm21nce3ld7cl1tr8j7h24jjlehuaoebt50) in this tutorial. The real fun is trying it out yourself; not going to take all the fun away from you.
> 

## In summary

Unison is already out there, Unison Cloud is either in public beta already or shortly there depending on when you’re watching/reading this. See the [attached links](https://www.unison.cloud/) and ask around.

I don’t know about you, but I’m looking forward to a future with no yamls, packaging, containers, version conflicts, json shuffling… Well, probably the last one is unavoidable, you still have to talk to the outside world, but still.

Less of all of that, and more coding — sounds like a good deal to me. Now, the only thing that’s left is to figure out how to replace meetings with unison…

---

{% embed https://youtu.be/F6S7NHnK1vA %}