In Part 1 of 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 for writing 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 the ability requirement in a few common ways:
- By interpreting the ability into a Unison data type or value
- By interpreting the ability into other abilities
Handling an ability
First let's try to call effectfulPredicate.stopIfTrue in a function that neither provides a handler nor passes the ability requirement up the call chain.
The following code tries to call effectfulPredicate.stopIfTrue in a pure function (remember, the {} means the function does not perform abilities).
nonEmptyName : Text -> {} Text
nonEmptyName name =
stopIfTrue (text -> text === "") nameThe 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 === "") nameIn the above example, how could the program actually interpret the underlying call to Abort.abort? 🤔 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. But Abort.abort has not been interpreted at this location in any of these ways, nor is the ability requirement represented 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 translate Abort into the Optional data type and then provide a sensible default.
usingAbilitiesPt1.nonEmptyName : Text -> Text
usingAbilitiesPt1.nonEmptyName name =
optionalName : Optional Text
optionalName =
toOptional! do
effectfulPredicate.stopIfTrue (text -> text === "") name
Optional.getOrElse "Unknown Name" optionalNameThere are a few things to call attention to in the code.
First, you'll notice that the Abort ability requirement is nowhere to be found in the signature for usingAbilitiesPt1.nonEmptyName. It's been eliminated by the toOptional! handler function.
Take a closer look at the signature of toOptional!:
toOptional! : '{g, Abort} a ->{g} Optional aThis function expects a delayed computation so it's not as simple as slotting effectfulPredicate.stopIfTrue in as an argument. Instead we need to delay result of calling effectfulPredicate.stopIfTrue. We do this by surrounding the entire expression we want delayed with parentheses and adding a single quote ' to the front.
With the call to Abort.abort interpreted into the Optional data type, we can then use standard functional combinators on Optional to provide a sensible default Text value for our function.
We might have also chosen to interpret Abort ability with Abort.toBug or toDefault!. Look at the similarities in the signatures of all three of these handlers:
toOptional! : '{g, Abort} a ->{g} Optional atoDefault! : '{g} a -> '{g, Abort} a ->{g} aAbort.toBug : '{g, Abort} a ->{g} aAll of them take in a delayed computation which performs the Abort 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 our store.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 signature store.stopIfTrue : (a -> Boolean) -> a ->{Abort, Store a} a
store.nonEmptyName : Text -> Textstore.nonEmptyName : Text -> Text
store.nonEmptyName name =
storeIsHandled : '{Abort} Text
storeIsHandled =
do
withInitialValue "Store Default Value" do
store.stopIfTrue (text -> text === "") name
abortIsHandled : Optional Text
abortIsHandled = toOptional! storeIsHandled
Optional.getOrElse "Optional Default Value" abortIsHandledIn the example above we're eliminating the Store ability first with the withInitialValue handler. It's a function which ensures the Store has been seeded with some value so if subsequent functions call Store.get they're guaranteed to return something.
Then we eliminate the Abort ability by transforming it into a Optional value.
One caveat: the order in which the handlers are applied can change the value returned! Handlers respect the rules of regular function application, so you might see them grouped with parentheses or function application operators when you're defining a Unison function that you intend to run in a watch expression or with the UCM run 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 or IO 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 to delay 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 become tryEmit : '{Stream Text}() or tryEmit : () -> {Stream Text}() without the syntactic sugar of the single quote.
Any one of these will work:
tryEmit1 : '{Stream Text}()
tryEmit1 = do Stream.emit "Hello World"
tryEmit2 : '{Stream Text}()
tryEmit2 _ =
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 to typecheck, but the application of a handler is the only way to run an effectful function in a watch expression.
The IO ability
Unison has a special built-in handler for functions which perform I/O. The IO ability is the only Unison ability whose handler is provided by the runtime, not by specific handler functions.
This function uses the readLine function in the base library to get the user's name and the printLine function to render it. Both are effectful and require the IO and Exception abilities. readLine returns a delayed computation, so in order get the Text value from the user we evaluate the thunk with the exclamation mark. The entire function is wrapped in a thunk because nameGreet _ 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 performs IO can only be called via the UCM with the run command.
It just so happens the run command provides the implementation details for handling both the IO and the Exception abilities, so we don't need a specific Exception handler in this case.
scratch/main> run nameGreet