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 : 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()