Custom effects require the effects crate feature because they build on the
Run wrappers, effect row macros, and handler infrastructure.
This guide shows the current manual pattern for writing a custom first-order
effect for the Run family. The point of documenting the manual path first is
to make the effect shape explicit before a future define_effect! macro hides
the boilerplate.
A first-order effect is an operation functor. Each operation stores either the
next value directly or a continuation that receives the operation result. A
Run<R, S, A> program stores those operations in the first-order row R; the
scoped row S is separate and is unused in the example below.
The pieces are:
- A zero-sized brand type.
- An operation enum parameterized by the continuation result
A. - An
impl_kind!block mapping the brand to the operation enum. - A
Functorimplementation that maps over the continuation result. - A
WrapDropimplementation describing whether a suspended operation can be collapsed when the surrounding program is dropped. - Named row aliases, usually with
define_effect_row_aliases!. - Smart constructors that lift operation values into a
Runprogram. - Handler-list entries that lower the custom operation into the remaining row.
For operations with an ordinary stored next value, WrapDrop::drop can return
Some(next). For operations that store a closure continuation, it usually
returns None, because there is no result value without running the operation.
This example defines a custom Config effect with one operation, ask_config.
The handler supplies a concrete configuration value, and the final assertion
checks the handled program output.
use fp_library::{
Apply,
Kind,
brands::CNilBrand,
classes::{
Functor,
WrapDrop,
},
define_effect_row_aliases,
handlers,
impl_kind,
kinds::*,
types::effects::{
Run,
scoped_nt,
},
};
struct ConfigBrand;
enum ConfigF<'a, A> {
Ask(Box<dyn FnOnce(&'static str) -> A + 'a>),
}
impl_kind! {
impl for ConfigBrand {
type Of<'a, A: 'a>: 'a = ConfigF<'a, A>;
}
}
impl Functor for ConfigBrand {
fn map<'a, A: 'a, B: 'a>(
f: impl Fn(A) -> B + 'a,
fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>),
) -> Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, B>) {
match fa {
ConfigF::Ask(reply) => ConfigF::Ask(Box::new(move |value| f(reply(value)))),
}
}
}
impl WrapDrop for ConfigBrand {
fn drop<'a, A: 'a>(
fa: Apply!(<Self as Kind!( type Of<'a, T: 'a>: 'a; )>::Of<'a, A>)
) -> Option<A> {
match fa {
ConfigF::Ask(_) => None,
}
}
}
define_effect_row_aliases! {
type AppEffects = first_order [ConfigBrand];
type NoEffects = first_order [];
}
type NoScopedEffects = CNilBrand;
type Program<A> = Run<AppEffects, NoScopedEffects, A>;
fn ask_config() -> Program<&'static str> {
Run::lift::<ConfigBrand, _>(ConfigF::Ask(Box::new(|value| value)))
}
fn greeting() -> Program<String> {
ask_config().bind(|name| {
Run::<AppEffects, NoScopedEffects, String>::pure(format!("Hello, {name}"))
})
}
let result = greeting()
.handle_with::<ConfigBrand, _, NoEffects>(
|op: ConfigF<'_, Run<NoEffects, NoScopedEffects, String>>| {
match op {
ConfigF::Ask(reply) => reply("Ada"),
}
},
)
.handle(handlers! {}, scoped_nt());
assert_eq!(result, "Hello, Ada");The ConfigBrand handler receives ConfigF<'_, Run<NoEffects, NoScopedEffects, String>>, not ConfigF<'_, String>. The operation's continuation has already
been rewritten so that continuing the custom operation produces a program in the
remaining row. In the example, reply("Ada") resumes the suspended program
after ask_config with the custom effect removed from the row.
Handle one custom effect at a time with
handle_with::<EffectBrand, _, RemainingRow>(...). When the first-order row
is empty, close the program with handle(handlers! {}, scoped_nt()). For
programs with several effects still in the row, keep handling one effect at a
time or close the whole row with handle(handlers! { ... }, scoped_nt())
once every remaining first-order and scoped effect has a handler.