Here we'll cover how to use abilities from a pragmatic point of view. We hope you've built up an intuition about abilities with the introduction to the mental model for abilities. In this document we'll talk about the parts of an ability, write some code that uses abilities, and learn how Unison abilities are represented in type signatures.
What and why
Abilities are Unison's answer to the question, "how should we model and manage computational effects in a language?" An ability pairs an interface which describes an effect's operations with handlers that dictate how the effect should actually be performed.
They offer a few benefits to the Unison programmer:
throw
an exception or emit
a value without an ability requirement appearing in the function signature, so you can reason about a program's behavior at the type level.traverse
when map
plus an ability requirement will suffice. (More on this later 😉.)A first encounter with Unison abilities
We'll dive in with an example of code which uses an ability. Let's look at the following function signature
effectfulPredicate.stopIfTrue : (a ->{g} Boolean) -> a ->{g, Abort} a
From the name and signature of this function, we might surmise that effectfulPredicate.stopIfTrue
takes in a value, tests it against some condition, and does not permit subsequent functions to continue if the condition returns true
. The computational effect here is "hey, potentially stop running the program." In other languages with different effect management strategies, you might model this by throwing an exception or returning the "missing" case of a data type representing "something or nothing."
Notice the {Abort}
attached to the function arrow ->
. We read the return type of this signature as saying: "stopIfTrue is a function which performs the Abort effect" in the process of returning some value of type a
. Abort
is what we call an ability in Unison and we say that the function effectfulPredicate.stopIfTrue
"requires the Abort ability" or has an "Abort ability requirement".
So how will we know what effectful operations are available for a given ability? To answer that we'll take a detour and look at how abilities are defined.
Ability declarations
Use the view
command in the UCM to .> view Abort
and you should see something like:
structural ability Abort
structural ability Abort where
lib.base.abilities.Abort.abort : {Abort} a
The code here is the ability declaration for Abort
. The keyword structural
or unique
specifies if the ability is unique by its name or by its structure --it's followed by the name of the ability and the keyword where
. Then you'll see one or more request constructors: type signatures that declare what operations an ability can perform. In this case, we see that the Abort
ability only declares one operation, Abort.abort
.
While you're here, check out a few more abilities in the UCM. Try .> view Stream
or .> view Store
to see their ability declarations. Abort
can Abort.abort
a program, Stream
can emit
a value, Store
can Store.get
or Store.put
a value, etc. These request constructors are the building blocks for code that uses abilities; you'll either be calling them directly or building off functions that call them directly. It's important to understand what these functions generally do but you'll notice there's no implementation here. You're just looking at an abstract interface for the effect being modeled. How the operation is performed is provided later by the ability's handler.
Let's look at the implementation of effectfulPredicate.stopIfTrue
to see how the Abort
ability is invoked.
effectfulPredicate.stopIfTrue : (a ->{g} Boolean) -> a ->{g, Abort} a
effectfulPredicate.stopIfTrue : (a ->{g} Boolean) -> a ->{g, Abort} a
effectfulPredicate.stopIfTrue predicate a =
if predicate a then Abort.abort else a
We can see in the if/else
clause that there's a call to the Abort.abort
function from the ability. If a function requires an ability, the function has access to the suite of operations that the ability defines.
What if we want to prevent an ability from being performed at all in our function?
🙅🏻♀️ We couldn't call any abilities from a function with a signature like:
stopIfTrue :(a -> Boolean) -> a -> {} a
The empty set of curly braces, {}
is how you specify that a function is "pure" or performs absolutely no effects.
That may not be what we want in this case though… effectfulPredicate.stopIfTrue
is pretty generic and we don't want to limit our user's style 😎. Let's pretend instead that we want to add a call to the Store
ability in our function. Combining multiple abilities in one function is easy to do in Unison by adding the ability to the curly braces separated by commas. This function stores a value before testing it with a predicate:
store.stopIfTrue : (a -> Boolean) -> a ->{Abort, Store a} a
store.stopIfTrue : (a -> Boolean) -> a ->{Abort, Store a} a
store.stopIfTrue predicate a =
Store.put a
if predicate a then Abort.abort else a
🤔 That's all very well and good but what if the caller of the function needs to perform an effect to actually run the predicate? We should represent that possibility in the type signature in some way so that callers of our function will know that it's ok to effectfully test the predicate. We can do that by adding a generic ability requirement to both the higher order function a -> Boolean
and the overall function return type a
.
effectfulPredicate.stopIfTrue : (a ->{g} Boolean) -> a ->{g, Abort} a
The key here is that the potential ability is a lowercase variable (like {g}
or {e}
) which needs to be in the signature of the predicate and in the return type of the overall function. Functions inherit the ability requirements of the functions that they call. That's why signatures like List.map : (a ->{𝕖} b) -> [a] ->{𝕖} [b]
have a generic ability requirement in both the transformation function and their return type. It's common to combine operations on Unison data structures like List
or Optional
with abilities for functional effect management!
Next we'll start trying to actually call these functions so we'll talk more about handlers and the rules for ability requirements in part 2 of this guide…