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 theintroduction to the mental modelfor 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 oremit
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
whenmap
plus anability requirementwill 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 thateffectfulPredicate.stopIfTrue
takes in a value, tests it against some condition, and does not permit subsequent functions to continue if the condition returnstrue
.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 whichperforms the Abort effect"in the process of returning some value of typea
.Abort
is what we call an ability in Unison and we say that the functioneffectfulPredicate.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 theview
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 theability declarationforAbort
.The keywordstructural
orunique
specifies if the ability is unique by its name or by its structure --it's followed by the name of the ability and the keywordwhere
.Then you'll see one or morerequest constructors:type signatures that declare what operations an ability can perform. In this case, we see that theAbort
ability only declares one operation,abort
.
While you're here, check out a few more abilities in the UCM. Try.> view Stream
or.> view Store
to see theirability declarations.Abort
canabort
a program,Stream
canemit
a value,Store
canStore.get
orStore.put
a value, etc. Theserequest constructorsare 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 generallydobut you'll notice there's no implementation here. You're just looking at an abstract interface for the effect being modeled.Howthe operation is performed is provided later by the ability's handler.
Let's look at the implementation ofeffectfulPredicate.stopIfTrue
to see how theAbort
ability is invoked.
We can see in theif/else
clause that there's a call to theabort
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 performedat allin 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 performsabsolutely 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 theStore
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 apredicate:
๐ค 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 toeffectfullytest the predicate. We can do that by adding a generic ability requirement to both the higher order functiona -> Boolean
andthe overall function return typea
.
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 predicateandin the return type of the overall function. Functions inherit the ability requirements of the functions that they call. That's why signatures likelib.base.data.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 likeList
orOptional
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โฆ