Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability handler counter concept #20

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions concepts/ability-handlers/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"blurb": "You can write your own ability handlers in Unison, providing behavior to a given computational effect",
"authors": [
"rlmark"
],
"contributors": []
}
56 changes: 56 additions & 0 deletions concepts/ability-handlers/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# About

In Unison, you can write your own abilities and handlers to supply different behaviors to your program. When a function calls one of the operations of an ability, Unison looks to the nearest enclosing handler to determine the next action to take. Handlers are special Unison functions where you can do things like pause and resume a computation, store state between calls to the ability, supply effectfully computed values to the rest of the program, or translate the ability into other Unison values. In short, they provide the implementation details for the ability.

Handlers follow some specific syntax conventions, but are conceptually a pattern match function. When writing a handler we are pattern matching on the request operations of the ability and dictating what should happen in each case when that operation is called.

```
structural ability KeyValue k v where
get : k -> Optional v
put : k -> v -> ()
```

A handler for the the `KeyValue` ability above will need to say what should happen when `KeyValue.put` and `KeyValue.get` are called in addition to showing what should occur when the a function is done calling the ability's operations, or never calls the operations in the first place.

## The parts of a handler

Let's look at a handler that enables interaction with a KeyValue store backed by an in-memory `Map`:

```
KeyValue.run : '{KeyValue k v} r -> r
KeyValue.run keyValueFunction =
impl : Map k v -> Request (KeyValue k v) r -> r
impl map = cases
{KeyValue.get k -> resume} -> handle resume (Map.get k map) with impl map
{KeyValue.put k v -> resume} -> handle resume () with impl (Map.put k v map)
{pure} -> pure
handle !keyValueFunction with impl Map.empty
```

Here's an overview of what this handler is doing:

1. This handler starts the `KeyValue` with an initial empty map value as the storage backing. This `Map` will be updated in subsequent calls to the request constructors of the ability.
2. The handler `get`'s values from the map when a program calls the `get` function and returns the expected value to the rest of the program
3. The handler `put`'s values into the map when a program calls the `put` function and updates the internal state of the handler by calling `impl` with an updated map
3. The handler passes through a `pure` value after all the interactions with the ability operations are done, or if the program doesn't end up using the ability at all.

### The type signatures of handlers

The type signature `KeyValue.run : '{KeyValue k v} r -> r` is read "KeyValue.run is a function which takes a computation that uses a `KeyValue` store in the process of returning some value, `r`, and eliminates the ability, allowing us to return that `r` value."

The single quote represents a thunk, or [delayed computation][delayed-computations].

Inside the `KeyValue.run` handler is a helper function with the signature `impl : Map k v -> Request (KeyValue k v) r -> r`. This is a common pattern for writing ability handlers. The helper function's first argument is the `Map` that we'll be updating to contain state internal to the handler. The second argument starting with `Request (KeyValue k v) r` is Unison's type which represents requests to perform the ability's operations (here those operations are `get` and `put`).

### Resuming computations

If you look at the cases in our pattern match, `{KeyValue.get k -> resume} -> ...`, you'll notice that we're doing more than just pattern matching on the operations of the ability like `get` or `put`, there's also a variable called `resume`; that's because a handler encapsulates a snapshot of the program state _as it is running._ Elsewhere in computer science literature the idea of "resuming computations" is called a [continuation][continuation-reference]. `resume` is a function whose argument is always the return type of the request operation in question, for example, `get : k -> Optional v` returns an `Optional v`, so that's the value provided to `resume` after looking up the key in the `Map`.

The fact that the continuation is reflected as a variable in the handler opens up possibilities for, say, rerunning the continuation, or even storing it!

### The handle ... with keywords

Many of the values in our handler have variable names that are up to us! For example, there's nothing magical about the word `pure` in the pattern match. You could call it `done` or `r` or `pineapple`. Likewise `resume` in the pattern match is just a convention. You could name it `theRestOfMyProgram` and call `handle theRestOfMyProgram` if you like. But there are two Unison specific keywords that have to be used in the handler: `handle ... with`. The `handle with` expression sandwiches two things: the first is the computation which performs the ability we're handling, and the second is the specific handler implementation which includes the `Request` type. Without it, there's nothing to communicate "hey we're operating on the request constructors of the ability" in the call to `!myKeyValueFunction` from the expression `handle !keyValueFunction with impl Map.empty`.

[continuation-reference]: https://en.wikipedia.org/wiki/Continuation
[delayed-computations]: https://www.unison-lang.org/learn/fundamentals/values-and-functions/delayed-computations/
14 changes: 14 additions & 0 deletions concepts/ability-handlers/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Introduction

In Unison, you can write your own abilities and handlers to supply different behaviors to your program. When a function calls one of the operations of an ability, Unison looks to the nearest enclosing handler to determine the next action to take. Handlers are special Unison functions where you can do things like pause and resume a computation, store state between calls to the ability, supply effectfully computed values to the rest of the program, or translate the ability into other Unison values. In short, they provide the implementation details for the ability.

Handlers follow some specific syntax conventions, but are conceptually a pattern match function. When writing a handler we are pattern matching on the request operations of the ability and dictating what should happen in each case when that operation is called.

```
structural ability KeyValue k v where
get : k -> Optional v
put : k -> v -> ()
```

A handler for the the `KeyValue` ability above will need to say what should happen when `KeyValue.put` and `KeyValue.get` are called in addition to showing what should occur when the a function is done calling the ability's operations, or never calls the operations in the first place.

10 changes: 10 additions & 0 deletions concepts/ability-handlers/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"url": "https://www.unisonweb.org/docs/abilities",
"description": "Language guide for ability handlers"
},
{
"url": "https://www.unison-lang.org/learn/fundamentals/abilities/",
"description": "Mental model for abilities"
}
]
20 changes: 18 additions & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@
"average_run_time": 2.9
},
"exercises": {
"concept": [],
"concept": [
{
"slug": "usher",
"name": "Usher",
"uuid": "5cb7e7e2-e5f1-418c-bd42-d56ead658e16",
"practices": [],
"concepts": ["ability-handlers"],
"prerequisites": [],
"difficulty": 2
}
],
"practice": [
{
"slug": "acronym",
Expand Down Expand Up @@ -242,7 +252,13 @@
}
]
},
"concepts": [],
"concepts": [
{
"uuid": "7791a83f-d824-49fb-9aad-38dd25f7144b",
"slug": "ability-handlers",
"name": "Ability Handlers"
}
],
"key_features": [
{
"title": "Content addressed",
Expand Down
20 changes: 20 additions & 0 deletions exercises/concept/usher/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Hints

## General

- Read about the [mental model for abilities][mental-model] in the intro to abilities documentation. It breaks down what your "internal computer" should be doing when executing code which uses abilities
- Take a look at [an example handler for the `Ask` ability][ask-handler]. It looks like it uses a helper function [Ask.provide.handler][ask-handler-implementation] that pattern matches on Ask's request constructor and a pure or pass-through case.

## 1. Implement a simple counter handler

- `Counter.run` is a handler which should contain some internal state: the current total count.
- Its customary for handlers to contain state as arguments to functions in the handler. Think about "updating the state" of the handler as "calling the handler again with an updated value"

## 2. Return the total count alongside the value produced by the function

- Experiment with the "pure" or "pass-through" case of the ability handler. What happens if you call "bug" instead of returning a value in the handler? Could you return _two_ instances of the value `{pure} -> (pure,pure)`, changing the type signature of the handlers accordingly?


[mental-model]: https://www.unison-lang.org/learn/fundamentals/abilities/
[ask-handler]: https://share.unison-lang.org/latest/namespaces/unison/base/;/terms/Ask/provide
[ask-handler-implementation]: https://share.unison-lang.org/latest/namespaces/unison/base/;/terms/Ask/provide/handler
85 changes: 85 additions & 0 deletions exercises/concept/usher/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Instructions

The following function makes use of Unison abilities to keep track of a running count of parties entering a theatre. 🎭

The `usher` function itself is fairly simple, it takes in a list of tickets representing a party and lets parties in the door until the theater is full. If a number of tickets in in a party is greater than the remaining seats, a separate overflow list is started, otherwise the total count is incremented and the party is added to the theater.

However, the original author has left the ability handler writing up to you! Without a handler providing the concrete behavior of the required `Counter` ability, the function cannot actually be run or tested.

```
usher : [Ticket] -> Nat -> Theater -> {Counter} Theater
usher party maxSeating = cases
Main room ->
currentTotal = !getCount
nextPartySize = List.size party
if currentTotal + nextPartySize > maxSeating then
Overflow party
else
incrementBy nextPartySize
Main (room ++ party)
Overflow room ->
Overflow (room ++ party)
```

The `Counter` ability is given to you as:

```
structural ability Counter where
getCount : () -> Nat
incrementBy : Nat -> ()
```

`getCount` is a "thunk" or ["delayed computation"][delayed-computations] that returns the current count when called.

`incrementBy` takes a number to increment the count by and returns `Unit`.

## 1. Implement a simple counter handler

You'll need to implement an ability handler called `Counter.run` that takes in the value to start the count from and determines what should be done when the request operations, `getCount` and `incrementBy`, are called.

```
Counter.run : Nat -> '{Counter} a -> a
Counter.run initialValue functionUsingCounter = todo "implement run"
```

Once this handler is implemented, we can use it in simple programs like:

```
program : Theater
program =
runUsher = 'let usher [Ticket, Ticket] 10 (Main [])
Counter.run 0 runUsher
```

Or even use our handler as a result of processing a list of attendees:

```
programWithList : Theater
programWithList =
attendees = [[Ticket, Ticket], [Ticket, Ticket, Ticket], [Ticket]]
runUsher = 'let List.foldLeft (theater -> party -> usher party 10 theater) (Main []) attendees
Counter.run 0 runUsher
```

A number of other examples of calling the `Counter.run` handler are included for testing!

## 2. Return the total count alongside the value produced by the function

Without changing the implementation of the `usher` function, we'd like to be able to get the total count as well as the result of the function using the `Counter` ability.

```
Counter.runWithTotal : Nat -> '{Counter} a -> (Nat, a)
Counter.runWithTotal initialValue functionUsingCounter = todo "implement runWithTotal"
```

Applying this to our particular case, this handler should return a tuple of the running total with the ending state of the `Theater`

```
programWithTotal : (Nat, Theater)
programWithTotal =
attendees = [[Ticket, Ticket], [Ticket, Ticket, Ticket], [Ticket]]
runUsher = 'let List.foldLeft (theater -> party -> usher party 10 theater) (Main []) attendees
Counter.runWithTotal 0 runUsher
```

[delayed-computations]: https://www.unison-lang.org/learn/fundamentals/values-and-functions/delayed-computations/
56 changes: 56 additions & 0 deletions exercises/concept/usher/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# About

In Unison, you can write your own abilities and handlers to supply different behaviors to your program. When a function calls one of the operations of an ability, Unison looks to the nearest enclosing handler to determine the next action to take. Handlers are special Unison functions where you can do things like pause and resume a computation, store state between calls to the ability, supply effectfully obtained values to the rest of the program, or translate the ability into other Unison values. In short, they provide the implementation details for the ability.

Handlers follow some specific syntax conventions, but are conceptually a pattern match function. When writing a handler we are pattern matching on the request operations of the ability and dictating what should happen in each case when that operation is called.

```
structural ability KeyValue k v where
get : k -> Optional v
put : k -> v -> ()
```

A handler for the the `KeyValue` ability above will need to say what should happen when `KeyValue.put` and `KeyValue.get` are called in addition to showing what should occur when the a function is done calling the ability's operations, or never calls the operations in the first place.

## The parts of a handler

Let's inspect a handler that allows interactions with a KeyValue store ability backed by an in-memory `Map`:

```
KeyValue.run : '{KeyValue k v} r -> r
KeyValue.run keyValueFunction =
impl : Map k v -> Request (KeyValue k v) r -> r
impl map = cases
{KeyValue.get k -> resume} -> handle resume (Map.get k map) with impl map
{KeyValue.put k v -> resume} -> handle resume () with impl (Map.put k v map)
{pure} -> pure
handle !keyValueFunction with impl Map.empty
```

Here's an overview of what this handler is doing:

1. This handler starts the `KeyValue` with an initial empty map value as the storage backing. This `Map` will be updated in subsequent calls to the request constructors of the ability.
2. The handler `get`'s values from the map when a program calls the `get` function and returns the expected value to the rest of the program
3. The handler `put`'s values into the map when a program calls the `put` function and updates the internal state of the handler by calling `impl` with an updated map
3. The handler passes through a `pure` value after all the interactions with the ability operations are done, or if the program doesn't end up using the ability at all.

### The type signatures of handlers

The type signature `KeyValue.run : '{KeyValue k v} r -> r` is read "KeyValue.run is a function which takes a computation that uses a `KeyValue` store in the process of returning some value, `r`, and eliminates the ability, allowing us to return that `r` value."

The single quote represents a thunk, or [delayed computation][delayed-computations].

Inside the `KeyValue.run` handler is a helper function with the signature `impl : Map k v -> Request (KeyValue k v) r -> r`. This is a common pattern for writing ability handlers. The helper function's first argument is the `Map` that we'll be updating to contain state internal to the handler. The second argument starting with `Request (KeyValue k v) r` is Unison's type which represents requests to perform the ability's operations (here those operations are `get` and `put`).

### Resuming computations

If you look at the cases in our pattern match, `{KeyValue.get k -> resume} -> ...`, you'll notice that we're doing more than just pattern matching on the operations of the ability like `get` or `put`, there's also a variable called `resume`; that's because a handler encapsulates a snapshot of the program state _as it is running._ Elsewhere in computer science literature the idea of "resuming computations" is called a [continuation][continuation-reference]. `resume` is a function whose argument is always the return type of the request operation in question, for example, `get : k -> Optional v` returns an `Optional v`, so that's the value provided to `resume` after looking up the key in the `Map`.

The fact that the continuation is reflected as a variable in the handler opens up possibilities for, say, rerunning the continuation, or even storing it!

### The handle ... with keywords

Many of the values in our handler have variable names that are up to us! For example, there's nothing magical about the word `pure` in the pattern match. You could call it `done` or `r` or `pineapple`. Likewise `resume` in the pattern match is just a convention. You could name it `theRestOfMyProgram` and call `handle theRestOfMyProgram` if you like. But there are two Unison specific keywords that have to be used in the handler: `handle ... with`. The `handle with` expression sandwiches two things: the first is the computation which performs the ability we're handling, and the second is the specific handler implementation which includes the `Request` type. Without it, there's nothing to communicate "hey we're operating on the request constructors of the ability" in the call to `!myKeyValueFunction` from the expression `handle !keyValueFunction with impl Map.empty`.

[continuation-reference]: https://en.wikipedia.org/wiki/Continuation
[delayed-computations]: https://www.unison-lang.org/learn/fundamentals/values-and-functions/delayed-computations/
10 changes: 10 additions & 0 deletions exercises/concept/usher/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"blurb": "Get started writing your own ability handlers by writing a counter",
"authors": ["rlmark", "pchuisano"],
"icon": "movie-goer",
"files": {
"solution": ["counter.u"],
"test": ["counter.test.u"],
"exemplar": [".meta/examples/counter.example.u"]
}
}
15 changes: 15 additions & 0 deletions exercises/concept/usher/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Design

## Learning objectives

- Learn how to write custom ability handlers in Unison
- Familiarize ability handler syntax
- Shows how handler can contain program state

## Out of scope

## Prerequisites

## Concepts

- `ability-handlers`
Loading