🤖 Introduction to Abilities: A Mental Model

One of Unison's more exciting language features is its support for abilities. They represent one of the ways in which a Unison program can manage computational effects in a program. Abilities are Unison's implementation of algebraic effects, as they're known in the literature 📚 - but don't worry, you don't need to have heard of them to use Unison abilities. In this section of the docs we'll build an intuition around the problems that algebraic effects are solving and form a mental model of what a program is doing when an ability is being called.

👋 As a heads up: we're not going to talk about the syntax specifics of Unison's abilities in this doc, if you're looking for that head to the section where we describe how to use an ability, this doc is just about creating an internal computer so that you have an understanding of how Unison executes code which uses Abilities.

What do we mean by effects

A computational effect or an "effectful" computation is one which relies on or changes elements that are outside of its immediate environment. Some examples of effectful actions that a function might take are:

  • writing to a database
  • throwing an exception
  • making a network call
  • getting a random number
  • altering a global variable

Loosely, you might think of an effectful computation as one which performs an action outside of its local scope compared to one which simply returns a calculable value. Sound like a large bucket? Yup, effects are important and common.

So when functional programmers talk about managing effects, they're talking about expressing the basic logic of their programs within some guard rails provided by data structures or programming language constructs. We'll put off a full discussion of functional effect management, but in general, managing effects in functional programming is done by representing the side effecting behavior in a more explicit way, therefore rendering it easier to react to it in a program. Unison's support for abilities is one such guard rail.

Abilities in pseudocode

Abilities are one way to express computations that perform effects, but comparatively few programming languages make use of them, so one of the challenges for learning about abilities is gaining an intuition of "how does this work 🧐" or more specifically, "does this look like anything I already know?" If you've encountered or written code that makes use of try and catch blocks, then you've seen a concept that offers a good starting point for an understanding of abilities.

Let's imagine the following code in a fictional language. 🪄

try {
  users = Database.getAllUsers
} catch {
  case DatabaseException message -> print message

We know from looking at this code that the function call Database.getAllUsers is performing an effect. It relies on the external resource of a database to gather the relevant entities and that resource might be unavailable or fail for some internal reason — represented by a DatabaseException.

We also know that there's some degree of safety in this code because of the try/catch mechanism. The try block let's us know we're executing code that may throw an exception, and the catch block is a handler for the exceptions that might surface.

At its most general, an ability is a pairing of some kind of effect with a handler which responds to that effect and says what to do when it happens.

Let's build on that understanding. Imagine we want to write something akin to a try/catch block, but rather than managing exceptions, we want to manage logging. Unlike exceptions, which mean that the program execution has stopped for some reason and can't continue with subsequent steps in the program, when a function creates the effect of writing a log line, it's typically in the process of doing something which should continue after the call to write a log.

In our fictional language, we might couple the ability to write logs with something that handles the log statement by printing them out and then resuming the program.

log {
   friends = getFriends user
   if (length friends) == 0
   then Logger.warn "user has no friends ☹️"
   else Logger.debug "user has friends"
   startParty friends user
 } handle {

Again, this language syntax doesn't exist, but we can assume the log block provides access to some kind of effectful logger. The code in the body of the block should log the user's number of friends and continue to execute the startParty friends user line of code.

Just like a try block is followed by a catch, imagine the log block is followed by a generic handle block, whose job is to pattern match on the possible actions that log can do. That might look like this:

log {
} handle {
 case (Logger.debug message -> remainderOfProgram ) ->
   print ("Debug: " ++ message)
 case (Logger.warn message -> remainderOfProgram ) ->
   print ("Warn: " ++ message)

Our handler needs some way to represent the rest of the program or the "continuation" of the program after the Logger call is made. We might represent this by adding it to the pattern match, that way, when the handler receives a call to Logger.warn or Logger.debug we can call the remainderOfProgram after our desired log statement and resume the snapshot of the state of the program when the call to the Logger is made. remainderOfProgram in this example is just the representation of continuing on to startParty friends user.

At this point we know that an ability combines some notion of an effect with a handler that manages the effect, where the handler can resume the computation that produced the effect.

So far, the effects that we've been exploring, Exceptions and Logging, don't actually change the behavior of the business logic that follows. But what if the effect we'd like to run is something like saving and accessing a global variable, or getting input from a user? The remainder of the program is likely to be impacted by that operation. In our mental model of abilities, we need a way for the effect to alter the state of the program.

This changes our idea of a handler a little bit. Previously, the handler only needed to capture two pieces of information to be effective:

  1. the operations of the ability that it's handling (i.e. Logger.debug or raised exceptions)
  2. some notion of finishing the computation that was in flight when an effect was performed.

Let's say we want to write a very simple program that puts and then gets a value from a key value store. In our fake language we might express the core logic of our program something like this:

kvStore {
   KVStore.put "id" 5
   KVStore.get "id"
 } handle with mapBasedStorage Map.empty

The block started by kvStore looks pretty uncontroversial, but handle looks different from a standard try/catch block. It looks like we're handing off the management of the effect to a function call which takes in some kind of in memory Map. 🤔 Abilities allow us to choose how we'd like to perform the effect in question from via swappable handler functions. We might decide to use a mapBasedStorage function or a fancyDistributedKVStore function to run our key value store operations. The handler of an ability is where you'll find the implementation details for performing the effect, and the state that the ability might rely on is often passed along in the form of arguments to the handler function.

You might see this pattern repeated in many functional programming contexts: abilities allow us to establish a separation of concerns between the code which creates the effect and the code which manages the behavior of that effect.

Let's think about what a handler should do to respond to a call to KVStore.put - we want to:

  1. grab the key and value arguments from KVStore.put when we're pattern matching on it.
  2. use them to update the map which is functioning as our data storage
  3. resume the remainder of the program with our chosen handler, mapBasedStorage, passing in the updated map

It might look something like this:

mapBasedStorage map =
 case (KVStore.put key value -> resume) ->
   updated = put key value map
   resume () with mapBasedStorage updated

Calling our mapBasedStorage function with an updated map after the put request effectively "sets" the handler's state for subsequent interactions with the key value store effect.

Now let's think about what a handler should do to respond to a call to KVStore.get.

  1. grab the key argument from KVStore.get in a pattern match.
  2. get it from our in-memory map based storage
  3. pass that value back to the program being executed
  4. ensure future calls to the key value store can be managed by the mapBasedStorage handler

It might look something like:

mapBasedStorage map =
 case (KVStore.get key -> resume) ->
   desiredValue = get key map
   resume desiredValue with mapBasedStorage map

Notice how the desired value from the map can be managed by a handler and then passed back to the continuation named resume. Think of resume as a function which is expecting the result of the ability operation being called as its argument. When the desired value is given to the resume function, it continues the main program.

An ability can resume the computation which performed it with a value tracked in the handler. Handlers allow us to pass information back to the program when an ability operation is called.


Roughly, an ability can be broken down into two things, an interface which specifies some operations, and handlers which provide behavior to those operations. When a program uses an ability, the program halts its execution, hops over to the responsible handle block, finds the matching operation, performs the behavior specified there, and then resumes the program.

Before going into the specificities of Unison's syntax for abilities, we wanted you to have an understanding of abilities on an operational level. In the next section about abilities, we'll talk more about how we use and represent abilities in actual Unison programs. We'll discuss how abilities are represented in function type signatures, what it means to handle an ability, and look at how they're defined.