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 bottleTypes
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 = 4Functions
Functions look like this:
Nat.inRange : Nat -> Nat -> Nat -> BooleanNat.inRange : Nat -> Nat -> Nat -> Boolean
Nat.inRange fromInclusive toExclusive x =
Universal.gteq x fromInclusive && Universal.lt x toExclusiveThe 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 isEvenFunctions can be defined inside other functions, and type signatures can be omitted:
List.takeWhile f xs =
go acc list = ...
go [] xsAbilities 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} BytesFunctions 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} VoidThunks
Unison makes heavy use of thunks, i.e. functions of shape:
() ->{SomeAbility} SomeTypeThere 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} sAn 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 pHandlers are just functions, and are called as such:
runStore 0 do
c = get()
put (increment c)
put (increment c)
toText get()