---
# System prepended metadata

title: 'Meetup 2024: Effects and associated effects'

---

---
title: "Meetup 2024: Effects and associated effects"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2024-09-10
url: https://hackmd.io/Oc8QQ8yGRcKvkicLh7kisg
---

# Meetup 2024: Effects and associated effects

## Background

Effects are, maybe, going to be the "next big thing" in programming languages.

They're compelling for a few different reasons.  One is that they give a type-based way to represent powerful control flow concepts (e.g. `shift`/`reset`, `call/cc`).  E.g., from RFC 3513, here's how Koka represents generators:

```koka
effect yield<a>
  fun yield(x : a) : ()

fun odd_dup(xs : list<int>) : yield<int> ()
  match xs
    Cons(x,xx) ->
      if x % 2 == 1 then
        yield(x * 2)
      odd_dup(xx)
    Nil -> ()
 
fun main() : console ()
  with fun yield(i : int)
    println(i.show)
    if i == 7: return // (presumably this causes control to return from `main`, not just a return back into `odd_dup`)
  list(1,20).odd_dup
```

(There is no library being used here and that `yield` is not a keyword or feature of the language.)

Those are "effect handlers".

What's we're more immediately interested in for Rust are "effect types" or "capabilities".  That is, we want a way to express what the body of a function is allowed to do.  E.g.:

- `total`: The function is deterministic, cannot panic or loop indefinitely, etc.
- `div`: The function may diverge.
- `exn`: The function may panic.
- `pure`: The function may panic or diverge.
- `ndet`: The function is non-deterministic.
- `gen`: The function may yield synchronously.
- `async`: The function may yield asynchronously.
- `nconst`: The function may rely on a runtime.
- etc.

There are number of effectful languages, e.g.:

- [Koka](https://koka-lang.github.io/koka/doc/book.html)
- [Ante](https://antelang.org/) (written in Rust)
- [Flix](https://flix.dev/)

## Syntax matters

Much of the pushback on effects in Rust has been, in my view, due to syntax proposals that have been very noisy.

## Associated effects

The Flix folks recently wrote a paper about [associated effects](https://dl.acm.org/doi/pdf/10.1145/3656393).

```flix
class Foldable [t : Type ] {
  type Aef : Eff // An associated effect synonym .
  type Elm : Type // An associated type synonym .
  def foldLeft (f: (b, Elm ) -> b \ ef , s: b, t: t): b \ Aef + ef
  def foldRight (f: (Elm , b) -> b \ ef , s: b, t: t): b \ Aef + ef
}
```

In Rust, something like:

```rust
trait Fold<T> {
  effect E1;
  type Item;
  fn fold_left<B, F>(self, init: B, f: F) -> B + (Self::E1 + F::E2) 
  where
    Self: Sized,
    F: FnMut<effect E2>(B, Self::Item) -> B + F::E2;
  ...
}
```

## Niko's "maximally minimal" do-based idea

https://nikomatsakis.github.io/rust-of-my-dreams/effects.html

I'm also, in some sense, less principled, in that I want to think of an effect as a "monad-like" combo of

* a transformation that takes place on the signature
* a transformation that takes place on the call to "undo" (possibly propagating) the 
* restrictions and/or extra ops on the body

```rust!
fn foo() -> u32
foo()

async fn foo() -> u32
foo().await : u32

const fn foo() -> u32
foo() /* no-op */

gen fn foo() -> u32
for _ in foo() { .. }

try fn foo() -> u32 // anyhow::Result<u32>
foo() -> anyhow::Result<u32> // (desugared form)
foo()? : u32                 // (nicer natural sugar)

// assumption: fully arbitrary orders of combinations
// of above are not useful, but there exist specific 
// useful orders, as shown below

async try fn foo() -> u32
foo() -> impl Future<Output = anyhow::Result<u32>>
foo().await?
```

```rust!
fn map<T, U>(
    v: Vec<T>,
    f: impl FnMut(T) -> U,
) -> Vec<U> {
    v.into_iter()
        .map(|x| f(x))
        .collect()
}

async fn async_map<T, U>(
    v: Vec<T>,
    f: impl async FnMut(T) -> U,
) -> Vec<U> {
    v.into_async_iter()
        .map(async |x| f(x).await)
        .collect()
        .await
}

const fn const_map<T, U>(
    v: Vec<T>,
    f: impl const FnMut(T) -> U,
) -> Vec<U>{
    v.into_const_iter()
        .map(const |x| f(x))
        .collect()
}
```

```rust!
do fn map<T, U>(
    v: Vec<T>,
    f: impl do FnMut(T) -> U,
) -> Vec<U> {
    v
        .into_iter()
        .map(do |x| f(x).do)
        .collect().do
}

do trait Iterator {
    do fn next(&mut self) -> Self::Item;

    do fn map<U>(
        self, 
        f: impl do FnMut(Self::Item) -> U
    ) -> impl do Iterator<Item = U>;

    fn size_hint(&self) -> usize;
}
```

Makes me think of all the non-try versions we implement using <https://stdrs.dev/nightly/x86_64-unknown-linux-gnu/core/ops/try_trait/struct.NeverShortCircuit.html> and calling the try versions.

You can write things as `do` to be generic over the details.

You can express combinators (e.g., fold) like this

```rust
trait Fold<T> {
  type Item;

  do fn fold_left<B>(
      self, 
      init: B,
      f: impl do FnMut(B, Self::Item) -> B,
  ) -> B
  where
    Self: Sized;
}
```

you could desugar this (not an actual syntax I think we need) conceivably to *something* like this

```rust
trait Fold<effect E, T> {
  type Item;

  E fn next(&mut self) -> B;

  E fn fold_left<B>(
      self, 
      init: B,
      f: impl E FnMut(B, Self::Item) -> B,
  ) -> B
  where
    Self: Sized;
}
```

I also want to have a simple set of possible effects that combine in predictable ways, roughly like so:

```
E = async? try(E)?+ const?
```

When you put an effect on a trait, it transforms all the methods in the trait equivalently.

Note: `const` is special (since its taking a effect/capability away, rather than adding one), and as part of its specialness, a definition that is meant to be generic over the const-effect needs to explicitly say so, e.g. via: `const do fn` (rather than just `do fn` as shown in all the cases above).

downsides:

* you cannot write a combinator with a function that takes a closure, calls it immediately (e.g., map), but doesn't capture it into the returned combinator
* Scott: A lot of `try_` variants on iterator change from `&self` to `&mut self`
    * Because try_ variant wants to mark self as cancelled??
    * Maybe we say do version is always `&mut self`, or something
    * Might also come up with Fn* traits; we might want to change to a different variant


## do Fn supersedes async Fn

Josh: If we make `do Fn` / `do FnMut` / `do FnOnce` exist and work as expected, then that would justify having `async Fn` / `async FnMut` / `async FnOnce` syntax.

## Disentangling

There are a number of concepts here that can be disentangled.

- Effect handlers
- Contexts
- Capabilities
- User-defined capabilities
- Generic effects
- Generic `do` ("maybe async")
    - Innovating here

TC: Different kinds of effect handlers:

- `for`: Handles an effect fully
- `?`: Handles and forwards an effect

## Non-goals (from Niko)

* gen as an effect
* taking a function that (a) has effects you want to be generic over but (b) which you will not immediately call (or at least you want to declare that you won't call)
* "removing" effects from the inputs ("handling them")

Precious comment from JT (regarding whether `gen` fits Niko's `do` model and it ending up being the List Monad): "just because it doesn't make sense doesn't mean Haskell hasn't done it"

## Maximally-maximally minimal

TC: Doing effect generics, with a fixed set of effects, and without `do` is more minimal.

The alternates here are:

- With `do` a single `impl` "magically" works for all effects.
    - `impl do Trait for ...`
    - `impl async Trait for T`
    - `impl Trait for T`
    - `impl const Trait for T`
- Without `do`, the same trait works with separate impls.
    
## Niko notes: what are our real goals?

Thinking about what we really want to get out of this...

* "Orthogonality"
    * Iterators, `?`, and friends all work the same way in every context
    * I want to be able to get back "yes you can have nice things" -- I want to use iterator APIs
* Oh and: const

```rust
try fn something() -> Vec<Result> {
    some_iter
        .iter()
        .map(|x| x.something_fallible()?)
        .collect()
}
```

libs-api really wants to not define `foo` and `try_foo` and `async_foo` and `async_try_foo` and...; libs-api has already rejected adding `try_*` variants of some methods because we're hoping for a better solution from lang.

## Breaking down the pieces

- `trait do Trait {}`
- `do fn foo()`
- `do async fn foo()`
- `impl Trait {} / impl async Trait {}`
- `impl do(!async) Trait for T {} / impl async do Trait for T {}`

## TC: What are the real goals?

TC: In my view, what we're most immediately trying to avoid is the `2^N` problem on traits, not on impls.  If we do nothing, then we need `2^N` traits.  Effect generics and associated effects avoid that.

The problem of `2^N` impls could be handled later by adding `do` on top of an effect generic design.  But it's hard to me to see a world where we wouldn't need the ability to write separate specific impls anyway.

Josh: I see this often in concrete functions, not just traits. e.g. `Vec` methods, `HashMap` methods.

## TC: What's most natural for Rust?

TC: My feeling is that much of the negative feedback to date has been due to some really noisy syntax that has been proposed.  It's hard to disentangle that.

TC: In terms of what's most natural for Rust, it seems the idea of traits defining a contract, for effects as they do elsewhere, and then having separate impls, is a natural framing.

TC: Conversely, the `do` is very magical, especially since it pushes against the idea of `Future`s as normal values in Rust.  This is an area where we do have specific non-syntax negative feedback.

## Control flow?

Could we define the "try" effect in a sufficiently general way that it also allow's the caller's closure to call `return` or `break` or `continue`? That should be possible using `ControlFlow`?

(Feedback: yes.)

## Handling things that can return errors or panic

Josh: Could we have a way to specialize (please pretend that's not a terrifying word) for try vs non-try, or have a construct that runs different code in the try vs non-try case? In particular, could we allow saying "if we're try, then return an allocation failure, otherwise panic with an allocation failure"? This implies having more than one `try` effect, some controlled by the caller and one controlled by the callee. `try(AllocationError)` vs `try(E)`

Niko: Yeah, we can make that work. Let's talk separately.

## Some notes

* do syntax for traits
    * what about default methods?
* ability to have...
    * generic code for the easy case
    * custom impls for specific cases
        * `impl async Iterator for X { ... }`

### Layer 1

```rust
do trait Iterator {
    type Item;
    
    do fn next(&mut self) -> Option<Self::Item>;
    
    do fn map<U>(
        self,
        op: impl do FnMut(Self::Item) -> U,
    ) -> do Map<U> {
        //  ^^^^^^ in reality we will need this
        Map {
            base_iter: self,
            func: op,
        }
    }
    
    do fn for_each(
        &mut self,
        op: impl do FnMut(Self::Item),
    ) {
        while let Some(x) = self.next().do {
            op(x).do;
        }
    }
}

do struct Map<I, F> {
    base_iter: I,
    func: F,
}

impl<R> do Iterator for do Map<I, F>
where
    I: do Iterator,
    F: do FnMut(I::Item) -> R,
{
    type Item = R;

    do fn next(&mut self) -> Option<Self::Item> {
        if let Some(i) = self.base_iter.next().do {
            let j = (self.func)(i).do;
            Some(j)
        }
        None
    }
}
```
