highlights
Sep 19, 2024

Pre-conference quick syntax guide

Rebecca Mark

You'll likely see a lot of Unison code during the Unison Forall conference. Here are some basics to get you started.

General vibe

Unison is a whitespace-sensitive language, here's what it looks like:

bottlesOfPop : Nat -> Text
bottlesOfPop bottleRange =
  List.range 0 bottleRange
    |> List.map (elem -> Nat.toText elem ++ " bottles of pop on the wall")
    |> Text.unlines

main: '{IO, Exception} ()
main = do
  bottles = bottlesOfPop 15
  printLine bottle

Types

Types in Unison have capital case letters, like User or Service.

Anything lowercase in a type signature is a type variable, e.g. the a in Optional a . In other languages that would be Optional<A> or Optional[A].

[a] is the type of lists, e.g. [User] is a list of users.

Nat is the type of positive integers.

Type signatures appear on the previous line of the definition, with a : :

four : Nat
four = 4

Functions

Functions look like this:

Nat.inRange : Nat -> Nat -> Nat -> Boolean
Nat.inRange fromInclusive toExclusive x =
  Universal.gteq x fromInclusive && Universal.lt x toExclusive

The return type is after the last -> in the signature, so Nat.inRange takes 3 Nat and returns a Boolean. Generic functions have type variables in their signature as lowercase letters:

List.map : (a -> b) -> [a] -> [b]

Functions are called via whitespace, with parentheses used for precedence:

Nat.inRange 4 10 6
Nat.inRange 4 10 (Nat.increment 5)

Lambdas are written as x -> ... :

List.map (x -> x + 1) [1, 2, 3]

The |> operator lets you call functions infix:

[1, 2, 3]
  |> List.map (x -> x + 1)
  |> List.filter isEven

Functions can be defined inside other functions, and type signatures can be omitted:

List.takeWhile f xs =
  go acc list = ...
  go [] xs

Abilities in signatures

Types in the signature with in curly braces, {Things, Like, This} are the abilities (think, "effects") that the function requires in order to run.

IO.console.printLine : Text ->{IO, Exception} ()
Random.bytes : Nat ->{Random} Bytes

Functions with abilities are called like regular functions, but the type system provides some guard rails by keeping track of which abilities are required.

Functions can also be generic in their ability list, which is also indicated by a lowercase type variable:

List.map: (a ->{g} b) -> [a] ->{g} b
Stream.iterate! : (a ->{g} a) -> a ->{g, Stream a} Void

Thunks

Unison makes heavy use of thunks, i.e. functions of shape:

() ->{SomeAbility} SomeType

There is syntactic sugar for thunks. In type signatures, thunks are written with a single quote '. In definitions, thunks are introduced by the do keyword followed by a block:

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

It's very common for functions to take thunks:

fork : '{IO} a ->{IO} ThreadId
catchAll : '{IO, Exception} a ->{IO} Either Failure a

catchAll do
  printLine "forking!"
  fork do
     ...

Thunks can be forced by having their name followed by ():

readLine: '{IO, Exception} Text

main: '{IO, Exception} ()
main = do
  a = readLine()
  b = readLine()
  printLine (a ++ b)

Abilities and handlers

Abilities are declared with dedicated syntax:

ability Store s where
  put : s ->{Store s} ()
  get : '{Store s} s

An ability is given behavior by a handler, which transforms the computation using that ability.

Handlers are written as recursive functions that use handle ... with cases to pattern match on the computation being handled:

Store.runStore: s -> '{Store s} a ->{IO, Exception} a
Store.runStore initial p =
  go state p = handle p() with cases
    { a } -> a
    { put newState -> resume } ->
      printLine "Writing state!"
      go newState resume
    { get _ -> resume } ->
      printLine "Getting state!"
      go state do resume state
  go initial p

Handlers are just functions, and are called as such:

runStore 0 do
  c = get()
  put (increment c)
  put (increment c)
  toText get()

You can read more about abilities here.