Using abilities part 1

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:

Abilities are visible in the type signature of functions, so the effectful behavior of your program is still represented in the type system. A function can't secretlythrowan exception oremita value without an ability requirement appearing in the function signature, so you can reason about a program's behavior at the type level.
They support a separation of concerns between the purpose of an effect and how the effect is ultimately executed. This facilitates easy testing of effectful components and prevents the core of a program from being polluted with logic for managing the effect.
They allow for a coding style that is relatively free of boilerplate and "type tetris" or "monadic plumbing ๐Ÿช "โ€”particularly where multiple effects are being performed at once. This means fewer "flatMaps" in favor of vanilla function application, or calls totraversewhenmapplus 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.stopIfTruetakes 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.Abortis 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 theviewcommand in the UCM to.> view Abortand 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 keywordstructuraloruniquespecifies 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 theAbortability only declares one operation,abort.

While you're here, check out a few more abilities in the UCM. Try.> view Streamor.> view Storeto see theirability declarations.Abortcanaborta program,Streamcanemita value,StorecanStore.getorStore.puta 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.stopIfTrueto see how theAbortability 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 else a

We can see in theif/elseclause that there's a call to theabortfunction 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.stopIfTrueis 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 theStoreability 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:

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 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 toeffectfullytest the predicate. We can do that by adding a generic ability requirement to both the higher order functiona -> Booleanandthe 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 likeListorOptionalwith 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โ€ฆ

๐Ÿ‘‰

To recap:

The abilities that a function performs are visible in curly braces{ }to the right side of the function arrow in a type signature.

Multiple ability requirements are represented in curly braces in a comma separated list:{Abort, Exception, Stream Text, g}

Generic ability requirements are typically single lowercase letter variables in curly braces like{e}

Pure functions perform no abilities and are represented with empty braces{}