๐Ÿ‘€ Unison at a glance

This document contains code snippets with a minimum of exposition. Links are provided throughout to more comprehensive docs sections. If you haven't downloaded the UCM, you might want to do that first. ๐Ÿ˜Ž


Hello World

Write your Unison code in any .u suffixed "scratch" file.

The classic helloWorld program performs console interactions via the IO ability. Read about how abilities model effects in Unison.

helloWorld : '{IO, Exception} ()
helloWorld = do printLine "Hello World"

Execute the entry point to your program with the run UCM command.

scratch/main> project.create hello-world
hello-world/main> run helloWorld

Or enter update to add the helloWorld program to the codebase, then run it by packaging it up in a binary executable.

hello-world/main> update
hello-world/main> compile helloWorld helloFile

This will produce an executable file called helloFile.uc in the same directory as the codebase. You can then start your program in your terminal.

$ ucm run.compiled helloFile.uc

๐Ÿ“šLearn more about executing Unison programs


Basic functions

The following introduces a function double with one parameter. Unison conventions for defining functions are detailed here

double : Nat -> Nat
double x =
 x * 2
> double 4

The > in a scratch file runs the double function in a watch expression.

๐Ÿ“šThe Unison tour walks through watch expressions and other workflow features


Delayed computation syntax

There are a few ways to indicate that a computation is delayed in Unison: do, an underscore argument, and the ' symbol.

main : '{IO, Exception} ()
main = do printLine "hello world"

main : '{IO, Exception} ()
main _ = printLine "hello world"

main : '{IO, Exception} ()
main = '(printLine "hello world")

Prepend ! to the delayed computation or append () to call it.

greet : '{IO, Exception} ()
greet = do
  name = console.readLine()
  printLine ("Hello " ++ name)

greet : '{IO, Exception} ()
greet = do
   name = !console.readLine
   printLine ("Hello " ++ name)

๐Ÿ“šA more detailed look at delayed computations


Text manipulation

Unison has a number of Text splitting and searching functions:

Text.filter isDigit "abc_10203_def" |> Text.split ?0
โงจ
["1", "2", "3"]

Use Pattern for more flexible regex-like text pattern operations.

Pattern.run (Pattern.capture (Pattern.many (chars "๐ŸŽ๐Ÿ"))) "๐Ÿ๐ŸŽ๐ŸŽ๐Ÿ123"
โงจ
Some (["๐Ÿ๐ŸŽ๐ŸŽ๐Ÿ"], "123")

๐Ÿ“šMore examples for the Pattern API


List literals

Square brackets introduce a Unison list.

[0, 1, 2, 3] List.++ [4, 5]
โงจ

The List.++ is our operator for list concatenation.

use List +: :+ head = 1 +: [2, 3, 4] head :+ 5
โงจ

Lists support various pattern matching options.

๐Ÿ“šLearn more about common collection types in Unison


List transformations

Nat.range 0 10 |> List.map (x -> x Nat.* 100) |> List.filter (const true) |> List.foldLeft (Nat.+) 0
โงจ
4500

The |> operator is a "pipe" which passes the result of executing the expression on the left as an argument to function on right.

The parenthesized x -> x * 100 argument to List.map is an example of lambdas in Unison.

๐Ÿ“šLearn more about operators like |>


if/else and pattern matching

The expression below is written with both if then and else syntax and with pattern matching syntax

isEven num =
  if mod num 2 === 0 then "even" else "odd"

isEven num = match num with
  n | mod n 2 === 0 -> "even"
  _ -> "odd"

Unison's pattern matching features include variable binding, pattern guards (separated by |), and as-patterns (indicated with an @).

match Some 12 with Optional.None -> "none" Some n| Nat.isEven n -> "n is a variable and | is a pattern guard" opt@(Some n) -> "opt binds to the entire optional value"
โงจ
"n is a variable and | is a pattern guard"

The cases syntax can take the place of a full match ... with expression.

foo n = match n with
  0 -> "zero"
  _ -> "not zero"

foo = cases
  0 -> "zero"
  _ -> "not zero"

๐Ÿ“šMore on pattern matching syntax in Unison.


Type declarations

A unison data type with uniqueness determined by its name:

type LivingThings
  = Animal
  | Plant
  | Fungi
  | Protists
  | Monera

A recursive Tree data type with a single type parameter:

structural type Tree a
  = Empty
  | Node a (Tree a) (Tree a)

The structural keyword means that types defined with the same structure are identical.

More on data types and the difference between structural and unique types.

Record types allow you to name the fields of your type.

type Pet = {
   age : Nat,
   species : Text,
   foodPreferences : [Text]
}

Creating a record type generates a series of helper methods to access and update the fields of the data type.

scratch/main> add Pet

 โŸ I've added these definitions:

     unique type Pet
     Pet.age                    : Pet -> Nat
     Pet.age.modify             : (Nat ->{g} Nat) -> Pet ->{g} Pet
     Pet.age.set                : Nat -> Pet -> Pet

๐Ÿ“šRecord type syntax in depth


Exception handling

nonZero : Nat ->{Exception} Nat
nonZero = cases
  n
    | n Nat.== 0  ->
      Exception.raise (Generic.failure "Zero was found" n)
    | otherwise   -> n
catch do nonZero 0
โงจ
Either.Left (Failure (typeLink Generic) "Zero was found" (Any 0))

An exception is "raised" with the Exception ability and "caught" with a handler.

๐Ÿ“šOur error handling with abilities doc describes this and other patterns in more detail.


Using abilities

Abilities are used for effect management in Unison.

getRandomElem : [a] ->{Abort, Random} a
getRandomElem list =
  index = Random.natIn 0 (List.size list)
  List.at! index list

This plucks a random element from the list by its index with Random.natIn, a function using the Random ability. If the index is not present in the list, it uses the Abort ability to halt execution.

splitmix and toOptional! are examples of ability handlers.

๐Ÿ“šUnderstand the mental model for abilities.


Distributed computations

Distributed computations can be expressed in the Unison language itself through the Remote ability, no special syntax or external framework needed.

Read about the Remote ability and its features

 forkedTasks : '{Remote} Nat
 forkedTasks = do
   task1 = Remote.fork here! do
     1 + 1
   task2 = Remote.fork here! do
     2 + 2
   Remote.await task1 + Remote.await task2

This example forks two computations to run remotely, then awaits their results and combines them.

๐Ÿ“šA more complete distributed use case has been fleshed out in this article.


Issuing an http request

Pull the library from Unison Share with the lib.install command.

myProject/main> lib.install @unison/http
exampleGet : '{IO, Exception} HttpResponse
exampleGet _ =
  uri =
    net.URI.parse "https://share.unison-lang.org/@unison/httpclient"
  req = do Http.get uri
  Http.run req

First we parse the URI, then we pass it to Http.get to get an HTTP response. The request is run by passing it to the Http handler.

๐Ÿ“šCheck out the library docs for more examples.


Basic file operations

Our standard library has a number of helpful File operations built in. They're located under the FilePath and Handle namespaces.


Concurrency primitives

Concurrency primitives like MVar, TVar, and STM are built into the base library. TVar and STM make it easy to write lock-free concurrent mutable data structures. For instance, hereโ€™s a simple lock-free queue implementation and a few helper functions:

type STM.TQueue a = TQueue (TVar [a]) (TVar Nat)

@source{ enqueue, dequeue}

The block introduced by STM.atomically below ensures that no one can access state of the queue until after the actions in the block have taken place.

queueExample : '{IO, Exception} ()
queueExample _ =
  runQueue : '{STM} Nat
  runQueue _ =
    queue = TQueue.fromList [1, 2, 3, 4, 5]
    enqueue 6 queue
    dequeue queue
    dequeue queue
  result = STM.atomically runQueue
  printLine (Nat.toText result)