If you want to write Unison applications and libraries, you are in the right place. The Unison language and ecosystem was thoughtfully designed to offer a delightful way to write code.
There are many ways to get started
๐ See the install and quickstart guide and just start coding
๐คฏ What's the big idea behind Unison?
๐ผ๏ธ Check out code hosting on Unison Share
๐ค๏ธ Looking to deploy your Unison program? Head to the Cloud
If you want a sense of what Unison code looks like, below are a few bread-and-butter language fundamentals with links to more comprehensive docs:
๐ 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.
Execute the entry point to your program with the run
UCM command.
scratch/main> project.create hello-world
hello-world/main> run helloWorld
Or add
your helloWorld
program to the codebase and run it by packaging it up in a binary executable!
hello-world/main> add
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
Basic functions
The following introduces a function double
with one parameter. Unison conventions for defining functions are detailed here
> 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 two ways to indicate that a computation is delayed in Unison. The first is the do
keyword:
main : '{IO,Exception}()
main = do
printLine "`do` is commonly seen at the start of an indented block of code"
The '
symbol is the second syntax option for introducing a thunk.
Stream.toList (Stream.map Nat.increment do emit 1)
Calling, or "forcing" the delayed computation also has two options. Prepend !
to the deplayed comptuation or append ()
to call it.
streamList : '[Nat]
streamList : '[Nat]
streamList = toDelayedList do Stream.range 10 15
!streamList
streamList()
List literals
Square brackets introduce a Unison list.
The List.++
is our operator for list concatenation.
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 Nat.* 100
argument to List.map
is an example of lambdas in Unison.
if/else and pattern matching
The expression below is written with both if then and else syntax and with pattern matching syntax
use Nat mod
isEven1 num =
if mod num 2 === 0 then true else false
isEven2 num = match num with
n | mod n 2 === 0 -> true
_ -> false
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 0
foo = cases
0 -> "zero"
_ -> "not zero"
foo 0โงจ"zero"
Type declarations
A unison data type with uniqueness determined by its name:
type LivingThings
A recursive Tree data type with a single type parameter:
structural type glance.Tree a
structural type glance.Tree a
= docs.glance.Tree.Empty
| docs.glance.Tree.Node a (glance.Tree a) (glance.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.
Record types allow you to name the fields of your type.
unique 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.
myProject/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
Exception handling
nonZero : Nat ->{Exception} Nat
nonZero = cases
n
| n Nat.== 0 ->
Exception.raise (Generic.failure "Zero was found" n)
| otherwise -> n
An exception is "raised" with the Exception
ability and "caught" with a handler.
Our error handling with abilities doc describes this pattern and more error types in detail.
Using abilities
Abilities are used for effect management in Unison.
getRandomElem : [a] ->{Abort, Random} a
getRandomElem : [a] ->{Abort, Random} a
getRandomElem list =
index = Random.natIn 0 (List.size list)
List.at! index list
toOptional! do splitmix 42 do getRandomElem [1, 2, 3, 4, 5]โงจSome 5
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.
Distributed computations
Distributed computations can be expressed in the Unison language itself through the Remote
ability. Read about the Remote ability and its features
This simple map/reduce code can operate over a distributed sequence, where the data may live in many different nodes in a cluster. This distributed computation use case has been fleshed out in an 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 : '{IO, Exception} HttpResponse
exampleGet _ =
uri =
net.URI.parse "https://share.unison-lang.org/@unison/httpclient"
req = do Http.get uri
Http.run req
The first part of this code uses data constructors from the http library to create a full uri out of an authority and path. The request is handled by passing it to the Http handler.
Basic file operations
readFileUtf8 : FilePath ->{IO, Exception} TextFilePath.writeFile : FilePath -> Bytes ->{IO, Exception} ()renameFile : FilePath -> FilePath ->{IO, Exception} ()
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 TQueue a
type TQueue a
= lib.base.IO.concurrent.STM.TQueue.TQueue (TVar [a]) (TVar Nat)
dequeue : TQueue a ->{STM} a
dequeue tq = match tryDequeue tq with
Optional.None -> retry()
Some a -> a
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} ()