https://github.com/lysxia/bluefin-algae
Algebraic effects in the Bluefin effect system
https://github.com/lysxia/bluefin-algae
Last synced: 11 months ago
JSON representation
Algebraic effects in the Bluefin effect system
- Host: GitHub
- URL: https://github.com/lysxia/bluefin-algae
- Owner: Lysxia
- License: mit
- Created: 2024-04-26T13:17:30.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-05-04T18:01:23.000Z (about 2 years ago)
- Last Synced: 2024-10-11T21:52:51.501Z (over 1 year ago)
- Language: Haskell
- Size: 121 KB
- Stars: 13
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
Named algebraic effect handlers in Bluefin
==========================================
This package leverages the delimited continuations primitives added in
GHC 9.6 to implement algebraic effects in the Bluefin effect system.
Algebraic effects are a minimalistic basis for **user-defined effects**.
Using algebraic effects, we can reimplement, from scratch, effects that
were built-in the Bluefin library, and more.
This is an experimental project. There are surprising performance
characteristics which may be problematic for practical applications.
[Details down below.](#quadratic-behavior-of-non-tail-recursion)
## Free monads in `IO`
An algebraic effect library is basically a free monad library with support for
extensible effects.
Effect handlers—the core primitive of algebraic effects—are conceptually
folds of trees, aka.
[`iter` in free](https://hackage.haskell.org/package/free-5.2/docs/Control-Monad-Free.html)
or [`cata` in recursion-schemes](https://hackage.haskell.org/package/recursion-schemes-5.2.2.5/docs/Data-Functor-Foldable.html#v:cata).
Effect systems—such as Bluefin—enable combinations of effects within a
single parameterized monad. Bluefin Algae seamlessly integrates with Bluefin's
infrastructure in order to compose algebraic effects.
The main novelties in Bluefin Algae are:
- computations use the same representation as `IO` (`State# s -> (# State# s, a #)`)
instead of recursive types or continuation-passing encodings.
This is possible thanks to the recently available primitives for delimited
continuations.
- thanks to Bluefin, effects are statically scoped: performing an operation
requires a handle which identifies a specific handler.
This enables new forms of abstraction boundaries.
A function `Eff s a -> Eff s a` cannot handle the operations of its argument.
The argument must be explicitly parameterized by the handler to allow
handling by its caller: `(forall z. Handler f z -> Eff (z : s) a) -> Eff s a`.
## Highlights
### Concurrency
In the following example, two threads yield a string back and forth, appending
a suffix every time.
```haskell
import Bluefin.Algae.Coroutine
pingpong :: Eff ss String
pingpong = withCoroutine coThread mainThread
where
coThread z0 h = do
z1 <- yield h (z0 ++ "pong")
z2 <- yield h (z1 ++ "dong")
yield h (z2 ++ "bong")
mainThread h = do
s1 <- yield h "ping"
s2 <- yield h (s1 ++ "ding")
s3 <- yield h (s2 ++ "bing")
pure s3
-- runPureEff pingpong == "pingpongdingdongbingbong"
```
Note that `coThread` and `mainThread` are just `IO` computations under the hood.
And we can interleave their executions without native multithreading. This is the
power of delimited continuations.
### Nondeterminism
With the ability to interrupt and resume operations freely, we can do
backtracking search in the `Eff` monad.
```haskell
import Bluefin.Algae.NonDeterminism as NonDet
pythagoras :: z :> zz => Handler Choice z -> Eff zz (Int, Int, Int)
pythagoras choice = do
x <- pick choice [1 .. 10]
y <- pick choice [1 .. 10]
z <- pick choice [1 .. 10]
assume choice (x .^ 2 + y .^ 2 == z .^ 2)
pure (x, y, z)
where (.^) = (Prelude.^) :: Int -> Int -> Int
-- runPureEff (NonDet.toList pythagoras) == [(3,4,5),(4,3,5),(6,8,10),(8,6,10)]
```
#### Backtracking and state
Resuming continuations more than once exposes the impurity of the
implementation of the built-in state effect in `Bluefin.State`.
Here is a program using nondeterminism and state. There are two branches
(`choose`), both modify the state (`incr`).
```haskell
import qualified Bluefin.State as B
nsExampleB :: [Int]
nsExampleB = runPureEff $ NonDet.toList \choice ->
snd <$> B.runState 0 \state -> do
_ <- choose choice True False
B.modify (+ 1) state
-- nsExampleB == [1,2]
```
The state handler (`runState`) is under the nondeterminism handler
(`NonDet.toList`), which suggests a state-passing interpetation, where the
original state is restored upon backtracking (both branches return `1`):
```haskell
nsExamplePure :: [Int]
nsExamplePure = runPureEff $ NonDet.toList \choice ->
let state = 0 -- initial state that was passed to runState
_ <- choose choice True False
let state' = state + 1 -- modify (+ 1)
pure state' -- (snd <$> runState) returns the final state
-- nsExamplePure == [1,1]
```
Because `Bluefin.State` is backed by `IORef`, the mutation persists
through backtracking (the second branch returns `2` in the first example).
In comparison, the state effect defined using algebraic effects
(`Bluefin.Algae.State`) has the state-passing semantics.
```haskell
import qualified Bluefin.Algae.State as A
nsExampleA :: [Int]
nsExampleA = runPureEff $ NonDet.toList \choice ->
A.execState 0 \state -> do
_ <- choose choice True False
A.modify (+ 1) state
-- nsExampleA == [1,1]
```
### Truly scoped exceptions.
The scoped exceptions from `Bluefin.Exception` are not completely scoped because
they can be observed by `bracket`. That is probably the right behavior in practice,
but makes the semantics of Bluefin less clear. For the sake of science,
`Bluefin.Algae.Exception` provides truly scoped exceptions, and implements
"`bracket`-observable" scoped exceptions on top.
## Lowlights
### Quadratic behavior of non-tail recursion.
For example, the following recursive counter will take time quadratic in `n`
because every call of `modify` traverses the call stack to find its handler
and capture the continuation.
```haskell
leftRecCounter :: z :> zz => Handler (State Int) z -> Int -> Eff zz ()
leftRecCounter _state 0 = pure ()
leftRecCounter state n = do
leftRecCounter state (n - 1)
modify state (+ 1)
```
## Comparison
### Bluefin
The Bluefin effect system provides a well-scoped [handle pattern][handle].
Unlike algebraic effects with which other computational effects can be
user-defined, Bluefin provides a collection of built-in effects
(state, exceptions, coroutines).
Without delimited continuations, only tail-resumptive algebraic effect handlers
are expressible in Bluefin. Those are effect handlers restricted to the
following form, which is equivalent to type `forall r. f r -> Eff ss r`.
```haskell
(\e k -> _ >>= k)
:: forall r. f r -> (r -> Eff ss a) -> Eff ss a
```
[handle]: https://jaspervdj.be/posts/2018-03-08-handle-pattern.html
## More reading
Named effect handlers are described in the literature in:
- [Binders by day, labels by night](https://maciejpirog.github.io/papers/binders-labels.pdf)
by Dariusz Biernacki et al.
- [First-class names for effect handlers](https://www.microsoft.com/en-us/research/uploads/prod/2021/05/namedh-tr.pdf)
by Ningning Xie et al. (impemented in the [Koka](https://koka-lang.github.io/koka/doc/index.html) language)
- [Effects, capabilities, and Boxes](https://dl.acm.org/doi/pdf/10.1145/3527320)
by Jonathan Brachtäuser et al.