FAQ's for abilities

Can I define an ability which references other abilities?

You cannot, at present, define an ability that directly specifies another ability requirement in its request constructors. Let's say I wanted to write a Cache ability. I might want to express something like:

-- 🙅🏻‍♀️ you can't do this
ability Cache k v where
  get : k -> {Cache k v, IO} v
  put : k -> v -> {Cache k v, IO} ()

After all, I know it's likely the users of my service will want to do IO as a part of handling their get and put requests. However, we can't specify that in the ability definition. The preferred way to allow for a Cache ability to perform IO and exceptions would be to write a handler that interprets theCacheability into theIOability.

Can I require two of the same abilities in one function?

No, you cannot require two of the same ability in a function.

You might expect that the following function would fail.

cannotDo : '{Stream Nat, Stream Nat} Nat
cannotDo = do 4

Which Stream would we handle first? But the function below is also not allowed despite the fact that the two abilities are parameterized by different types.

cannotDoTypeConstructor : '{Store Nat, Store Text} Nat
cannotDoTypeConstructor = do 4

Our type system does not currently support this.

What doesRequestmean?

TheRequesttype is the type that Unison uses to model therequest constructorsof an ability.

In the expressionhandle !myEffect with myEffectHandlerthe termmyEffectHandlerwould have a type which lookssomethinglikeRequest (MyEffect) a -> a- The exact semantics are described in thelanguage guide section on handlers.

When defining your own handlers you might see theRequesttype, but callers of handler functions shouldn't typically need to manage it directly.

Why can't an ability be at the "top-level" of a value?

For example, you can't do:myTerm = printLine "Hi".

Currently values at the "top level" of a program have to be pure, so it's more common to seedelayed computationsthat contain abilities (expressed with the single quote syntax). This may change in a future version of Unison. Think of the ability constraint as a constraint onthe function arrowas opposed to a constraint on a value.

How do I test something which requires the IO ability?

Run a single test which performs IO with theio.test command.Theio.testcommand expects adelayed computationwithIOas an ability requirement. The rest of the Unisontesting conventionsremain unchanged.

You can also test functions which perform IO with Unison'stranscriptsby interleaving Unison code which performs IO with UCM fenced codeblocks that call theruncommand.

A document about writing Unison transcriptscan be found here.

How do you express failures when writing a custom ability?

For example, if you write aHttpability or aCacheability, you may want to capture the fact that a request may fail.

It's tempting to write:

-- 🙅🏻‍♀️ you can't require an ability in the definition of an ability like this
 unique ability Http where
   handleRequest : Request -> {Exception} Response

Instead, here are a few strategies that you'll see employed:

  • Rather than representing the failure in the definition of the ability itself, thehandlersof the ability can account for failure, calling other abilities that represent a failure state, or by using data types that represent the failure.
  • You might use a data type likeOptional,orEitherin the type signatures of your ability's operations themselves.
  • Some abilities contain a specific request constructor that represents failure.For example, the Tokenizer ability on shareThis design decision is less common. A heuristic to use when deciding whether to build a "fail" operation into your ability is if the "fail" operation is one of the key behaviors of the effect you're trying to model, as opposed to an operational hazard.

Why does a handler need a case which doesn't refer to the ability operations?

Handlers have a case in their pattern match which takes the form{r} -> ....

This case needs to be present for the following reasons:

  • It yields the last executed value of the block being handled to the rest of the program.
  • It handles situations where an ability might berequiredby a function but is not ultimately used.

For example, this function won't always need to callabort:

errorHandling.divBy : Nat -> Nat ->{Abort} Nat
errorHandling.divBy a b = match b with
  0 -> abort
  n -> a Nat./ b

A handler for this function might look like:

Abort.toOptional! : '{g, Abort} a ->{g} Optional a Abort.toOptional! f = handle !f with cases { r } -> Some r { abort -> resume } -> None Abort.toOptional! '(errorHandling.divBy 4 2)

Without the{r} -> ...case, you cannot typecheck the handler.

Pattern match doesn't cover all possible cases:
      2 | Abort.toOptional! f = handle !f with cases
      3 |   {abort -> resume} -> None

Patterns not matched:
 * { _ }

What's the relationship between abilities and monads?

That deserves a longer discussion. The short answer is that abilities are as expressive as monads.

But for nowcheck out this gist!

I see some code which leaves off the ability requirements in an ability declaration, what gives?

When defining an ability's operations, it's often a nice shorthand to leave off the implied ability requirement—that is, the{Abort}inabort : {Abort} a—afterall, we know that an ability operation will be performing the ability in question and will require a handler. Unison will fill that in for us if we omit it, so we could have also defined the type signature of the abort operation asAbort.abort : x.

🚧🚧 🏗 More coming soon 🚧🚧