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 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:

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 secretly 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.
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 to 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.

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 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 else a

We can see in the if/else clause that there's a call to the 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 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 lib.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 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…

👉

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 {}