Effects

An Effect is a struct that represents a request for a side effect. It's a message from your computation to the outside world.

Defining effects with #[effect]

To define an effect, annotate a struct with #[effect(ResumeType)]. The resume type defines what value the computation receives back after the effect is handled. A bare #[effect] is shorthand for #[effect(())].

use corophage::prelude::*;

#[effect(())]
pub struct Log(pub String);

#[effect(String)]
pub struct FileRead(pub String);

#[effect(Never)]
pub struct Cancel;

The macro supports lifetimes, generics, named fields, and borrowed resume types:

// Lifetime parameters
#[effect(bool)]
pub struct Borrow<'a>(pub &'a str);

// Generic parameters
#[effect(T)]
pub struct Generic<T: std::fmt::Debug + Send + Sync>(pub T);

// Named fields
#[effect(Vec<u8>)]
pub struct ReadDir { pub path: String, pub recursive: bool }

// The resume type may reference the GAT lifetime 'r
#[effect(&'r str)]
pub struct Lookup(pub String);

A resume type that borrows a value of a type parameter, such as &'r T, needs that parameter to be 'static:

#[effect(&'r T)]
pub struct Get<T: Send + Sync + 'static>(pub std::marker::PhantomData<T>);

Only the type parameter must be 'static. The handler can still resume with a short-lived borrow, such as a reference to a local value, and the program remains Send. Without the bound, the compiler reports that T may not live long enough. Removing the requirement would break Send for effects with lifetime parameters because of a current compiler limitation (rust-lang/rust#100013).

Manual Effect implementation

If you need to implement the Effect trait by hand (without the macro), you must provide both the Resume associated type and the shorten_resume method:

struct Ask(pub String);

impl Effect for Ask {
    type Resume<'r> = &'r str;

    fn shorten_resume<'a: 'b, 'b>(resume: &'a str) -> &'b str {
        resume
    }
}

The shorten_resume method witnesses that Resume<'r> is covariant in 'r -- that is, a resume value with a longer lifetime can be safely used where a shorter lifetime is expected. This is required by invoke to allow sub-programs to borrow from shorter-lived data than the outer program.

The body is always just resume. The #[effect] macro generates this automatically, so you only need to write it when implementing Effect by hand.

Effect sets with Effects!

Effects are grouped into sets using the Effects! macro:

type MyEffects = Effects![Log, FileRead, Cancel];

This creates a type-level list (coproduct) of effects. The type system tracks which effects have been handled and prevents you from running a program until all effects have handlers.

You can also compose effect sets using the ...Alias spread syntax to splice in an existing type alias:

type IoEffects = Effects![Log, FileRead];
type AllEffects = Effects![Cancel, ...IoEffects];
// Equivalent to: Effects![Cancel, Log, FileRead]

The spread must appear as the last argument. This follows the same convention as frunk's Coprod!(...Tail) syntax.

Generic effect sets

Use the Effects<'a> trait when a helper should accept any effect set. The trait and the Effects! macro share a name and are both available from the prelude:

use corophage::prelude::*;

fn identity<'a, Es: Effects<'a>, F>(
    program: Program<'a, Es, F>,
) -> Program<'a, Es, F> {
    program
}

Effects<'a> is implemented automatically for effect coproducts whose payloads satisfy Send + Sync + 'a, including Effects![]. The lifetime permits effects to borrow local data. It does not require their resume values to be Send or Sync; sendable execution checks those requirements separately.

A generic helper that invokes a callback's program also needs bounds to forward its effects and return the corresponding resumes. Resumes<'r, Es> names the coproduct of the effects' Resume<'r> types, in the same order as Es. Its resume lifetime is independent of the effect-set lifetime.

use corophage::{EmbedEffect, ProjectResume, Resumes};
use std::future::Future;

fn invoke_callback<'a, Es, F, Make, Indices>(
    make: Make,
) -> Program<'a, Es, impl Future<Output = F::Output> + 'a>
where
    Es: Effects<'a> + EmbedEffect<Es, Indices>,
    Resumes<'a, Es>: ProjectResume<'a, Es, Indices>,
    F: Future + 'a,
    Make: FnOnce() -> Program<'a, Es, F> + 'a,
{
    Program::new(|mut y: Yielder<'a, Es>| async move {
        y.invoke(make()).await
    })
}

EmbedEffect forwards a child's effect into the parent set; ProjectResume returns the matching response to the child. Use the same generic Indices parameter in both bounds. These bounds are needed even when both sets are Es; concrete call sites infer the indices. When the child uses a subset of the parent's effects, use SubEffs: EmbedEffect<OuterEffs, Indices> and Resumes<'a, OuterEffs>: ProjectResume<'a, SubEffs, Indices> instead.

These public bounds support generic program construction and invocation. Generic handler-list assembly and execution have additional dispatch bounds.