InPart 1of the guide for using Unison abilities, we covered how to call ability operations and represent abilities in type signatures. This document will describe how to apply existing handlers from the base library to programs which use Unison abilities.👋As a heads up, this doc doesn't go into conventions forwriting your own handlers- it's just about using handlers.
What is a handler?
A handler is a Unison function which supplies the implementation or behavior for a given effect.
Handlers eliminate theability requirementin a few common ways:
- By interpreting the ability into a Unison data type or value
- By interpreting the ability intoother abilities
Handling an ability
First let's try to calleffectfulPredicate.stopIfTrue
in a function that neither provides a handler nor passes the ability requirement up the call chain.
The following code tries to calleffectfulPredicate.stopIfTrue
in a pure function (remember, the{}
means the function does not perform abilities).
nonEmptyName : Text -> {} Text
nonEmptyName name =
stopIfTrue (text -> text === "") name
The error we get back is:
The expression in red needs the {Abort} ability, but this location does not have access to any abilities.
4 | stopIfTrue (text -> text === "") name
In the above example, how could the program actually interpret the underlying call toabort
?🤔 We might want to abort by crashing the program, or we could abort by translating the ability into a data type, or we could abort by translating the operation into another ability that might be handled elsewhere. Butabort
has not been interpreted at this location in any of these ways, nor is theability requirementrepresented in the signature of the function that calls it.
Fortunately, the Unison standard library provides handlers for the abilities we've seen, so let's translateAbort
into theOptional
data type and then provide a sensible default.
usingAbilitiesPt1.nonEmptyName : Text -> Text
usingAbilitiesPt1.nonEmptyName name =
optionalName : Optional Text
optionalName =
toOptional!
'(effectfulPredicate.stopIfTrue (text -> text === "") name)
Optional.getOrElse "Unknown Name" optionalName
There are a few things to call attention to in the code.
First, you'll notice that theAbort
ability requirement is nowhere to be found in the signature forusingAbilitiesPt1.nonEmptyName
.It's been eliminated by thetoOptional!
handler function.
Take a closer look at the signature oftoOptional!
:
toOptional! : '{g, Abort} a ->{g} Optional a
This function expects adelayed computationso it's not as simple as slottingeffectfulPredicate.stopIfTrue
in as an argument. Instead we need to delay result of callingeffectfulPredicate.stopIfTrue
.We do this by surrounding theentire expressionwe want delayed with parentheses and adding a single quote'
to the front.
With the call toabort
interpreted into theOptional
data type, we can then use standard functional combinators onOptional
to provide a sensible defaultText
value for our function.
We might have also chosen to interpretAbort
ability withAbort.toBug
ortoDefault!
.Look at the similarities in the signatures of all three of these handlers:
toOptional! : '{g, Abort} a ->{g} Optional a
toDefault! : '{g} a -> '{g, Abort} a ->{g} a
Abort.toBug : '{g, Abort} a ->{g} a
All of them take in a delayed computation which performs theAbort
effect and return a value with the ability requirement removed.
Handling multiple abilities
We've seen how simple it is for functions to perform multiple effects at once with ourstore.stopIfTrue
code. When a function performs multiple effects it's common to nest handlers inside one another, with each handler peeling off one ability requirement. Handlers are just functions after all!
As a refresher the abilities we are trying to eliminate are in the signaturestore.stopIfTrue : (a -> Boolean) -> a ->{Abort, Store a} a
store.nonEmptyName : Text -> Text
store.nonEmptyName : Text -> Text
store.nonEmptyName name =
storeIsHandled : '{Abort} Text
storeIsHandled =
'(withInitialValue
"Store Default Value"
'(store.stopIfTrue (text -> text === "") name))
abortIsHandled : Optional Text
abortIsHandled = toOptional! storeIsHandled
Optional.getOrElse "Optional Default Value" abortIsHandled
In the example above we're eliminating theStore
ability first with thewithInitialValue
handler. It's a function which ensures theStore
has been seeded with some value so if subsequent functions callStore.get
they're guaranteed to return something.
Then we eliminate theAbort
ability by transforming it into aOptional
value.
One caveat: the order in which the handlers are applied can change the value returned! Handlers respect therules of regular function application,so you might see them grouped with parentheses orfunction application operatorswhen you're defining a Unison function that you intend to run in awatch expressionor with the UCMrun
command.
Abilities and top level definitions
🎨It's common to express the core of your Unison program in terms of abilities and translate your abilities into Unison standard data types orIO
because abilities cannot be left unhandled "at the top level" of a file
🙅🏻♀️ A term like this will not typecheck:
tryEmit : {Stream Text}()
tryEmit = Stream.emit "Hi"
The quick solution for this is todelay the computation,with a thunk, so that the{Stream Text}
abilities requirement is once again found on the right side of the arrow. With a delayed computation, the signature would becometryEmit : '{Stream Text}()
ortryEmit : () -> {Stream Text}()
without the syntactic sugar of the single quote.
Any one of these will work
tryEmit1 : '{Stream Text}()
tryEmit1 _ = Stream.emit "Hello World"
tryEmit2 : '{Stream Text}()
tryEmit2 = 'let
Stream.emit "Hello World"
tryEmit3 : '{Stream Text}()
tryEmit3 = '(Stream.emit "Hello World")
Note that the trick of adding a thunk to a value that performs an ability will enable your code totypecheck,but the application of a handler is the only way to run an effectful function in awatch expression.
The IO ability
Unison has a special built-in handler for functions which perform I/O. TheIO
ability is the only Unison ability whose handler is provided by the runtime, not by specific handler functions.
This function uses thereadLine
function in the base library to get the user's name and theprintLine
function to render it. Both are effectful and require theIO
andException
abilities.readLine
returns a delayed computation, so in order get theText
value from the user we evaluate the thunk with the exclamation mark. The entire function is wrapped in a thunk becausenameGreet _
is syntactic sugar for a delayed computation, so the final signature of the expression is:
This signature is important for the IO handler. That's because at the top level of a Unison program, a function which performsIO
can only be called via the UCM with therun
command.
It just so happens therun
command provides the implementation details for handling both theIO
and theException
abilities, so we don't need a specificException
handler in this case.
.> run nameGreet