Introduction
An Algebraic Effects System for Rust.
API surface is very much in flux and evolving.
New effects will be added as they are discovered to be useful in the Rust ecosystem.
How are Algebraic Effects useful?
Algebraic Effects are useful because they allow programs to be expressed not only in terms of what kind of value they can compute but also on what possible side-effects or external resources will such a computation require.
By using Effect Handlers, the interpretation of how an effect is performed is independent of the program description. This means that a single program description can be interpreted in different ways. For example, using a test-handler that mocks request to external services, or using a live-handler that actually performs such requests.
If you want to read more about different language implementations and theory behind effects, read the effects-bibliography.
Fx.rs
is inspired by the following two implementations, and uses a similar notion of the Handler, Ability, and Effect concepts:
- Unison Abilities
- Kyo (Scala3) - special thanks to @fbrasisil, Kyo's author who kindly provided a minimal kyo-core that helped me understand algebraic effect systems and inspired this library.
- [Fx.go] my original library for Golang.
However, Fx.rs
has a different surface API since we are trying to provide the best dev-experience for Rust programmers.
Concepts Tour
This section introduces the concepts of Effects, Abilities, and Handlers as present in fx-rs
.
No prior experience with other effect systems is required. We explain concepts from simple to advanced, using Rust idioms.
Effects
An Effect (Fx<S, V>
, read: "V given S") is a description of a program that computes V
, provided that the requirement S
is present.
Effects are descriptions of programs; they compute nothing and produce no side-effects until they are evaluated, once all requirements are met.
A common analogy is a recipe: you have a description of steps and a list of requirements (ingredients and utensils). Once you have them all, you can perform the recipe.
Abilities
In Fx<S, V>
, S
is the Ability (sometimes called the set of Abilities, Capabilities, or Effect Environment) needed to compute V
.
Abilities describe the external resources or side-effects possible while computing V
.
Examples:
- network abilities (e.g., HTTP requests)
- console abilities (e.g., printing, reading input)
- non-deterministic abilities (e.g., random numbers)
- resource handling (e.g., managing shared resources)
- exception handling (e.g., interruption, finalizers)
- anything else that interacts with the outside world
Handlers
A Handler for an ability is a particular interpretation of what that ability means.
Handlers are the only side-effectful part of your programs. You can have different handlers for the same ability, and each handler decides how and when to perform world-modifying side-effects.
For example, for an HTTP ability, you can have a test handler that mocks responses for tests, or a live handler that performs real network requests in production.
Basic Effects in fx-rs
This section expands on Concepts and shows how they relate to the fx-rs
API, providing intuition on effect requirements and evaluation in Rust.
Fx<(), V>
: Immediate Effects
The most basic effects are immediate effects. These are of type Fx<(), V>
, meaning they have no ability requirements and evaluate to a value V
.
Immediate effects are created using Fx::pure(value)
or Fx::value(value)
.
A pure effect just holds an already known value instead of computing it.
The value can be retrieved by calling .eval()
on the effect. Only effects with no requirements (()
) can be evaluated directly.
#![allow(unused)] fn main() { use fx::Fx; // Type alias for a simple effect with no requirements type PureString = Fx<(), String>; let v = "Hello World".to_owned(); let effect: PureString = Fx::pure(v.clone()); let result: String = effect.eval(); assert_eq!(result, v); // result: String } }
Fx<S, V>
: Pending Effects
An effect Fx<S, V>
where S
is not ()
is a pending effect that needs S
to be provided before computing V
.
The most basic pending computation is a function. For example, a function from String
to usize
can be expressed as an effect of type Fx<String, usize>
:
#![allow(unused)] fn main() { use fx::Fx; fn length_of_string(s: String) -> usize { s.len() } fn func_example() { let effect: Fx<String, usize> = Fx::func(length_of_string); let requirement = "Hello World".to_owned(); let provided = effect.provide(requirement.clone()); let result = provided.eval(); assert_eq!(result, requirement.len()); } }
Fx::func(f)
produces a pending effect of typeFx<S, V>
from a functionf: S -> V
..provide(value)
discharges the requirement and returnsFx<(), V>
. No computation is performed until.eval()
is called..eval()
performs the computation, since all requirements have been provided.
These are the most basic effects in fx-rs
. More interesting effects are presented in later chapters.
Core Effects
The list of effects provided by fx-rs
will grow as new needs are discovered in Rust programs.
Current ideas for contributions:
- Resource management (safe acquire/use/release of resources)
- Structured concurrency (beyond manual mutex/channels)
- Any other effectful pattern useful in Rust—issues and PRs are welcome!
#![allow(unused)] fn main() { // Type alias for a function effect type StringToUsize = Fx<String, usize>; fn length_of_string(s: String) -> usize { s.len() } let effect: StringToUsize = Fx::func(length_of_string); let requirement = "Hello World".to_owned(); let provided = effect.provide(requirement.clone()); let result: usize = provided.eval(); assert_eq!(result, requirement.len()); // result: usize }
Effect Requirements
In fx-rs
, effect requirements are the types that must be present in the environment for an effect to be evaluated. Requirements can be provided in any order, and combinators exist to manipulate and discharge them.
See the subchapters for details on applying, shuffling, and providing requirements in Rust.
Functional Requirements
As seen in the previous Context chapter, you can require any type to be part of an effect's environment.
You can also require functions of some type to be present and use them in your program descriptions without knowing their exact implementation.
For example, suppose we require a function from usize
to usize
and then apply it to a value:
#![allow(unused)] fn main() { // Type alias for a function requirement type FnReq = Fx<fn(usize) -> usize, usize>; fn double(n: usize) -> usize { n * 2 } // The effect requires a function of type fn(usize) -> usize let effect: FnReq = Fx::require(); // Provide the function implementation as the requirement let applied = effect.provide(double); // Provide the input value to the function let result = applied.provide(12).eval(); assert_eq!(result, 24); // result: usize }
For more complex cases, you can nest effect requests and flatten them using combinators, as in the fx-rs API.
Shuffling And
ed Requirements
In fx-rs
, requirements are identified by their type, not by name or position. Effect requirements can be freely rearranged with no impact on program meaning.
And
Combinators
Several combinators help you rearrange and manipulate And
ed effect requirements:
#![allow(unused)] fn main() { use fx::Fx; // Type alias for collapsed requirements type Collapsed = Fx<u32, u32>; let fx: Fx<(u32, u32), u32> = Fx::immediate((10u32, 10u32), 20u32); let fx2: Collapsed = fx.and_collapse::<u32>(); let result: u32 = fx2.provide(10).eval(); assert_eq!(result, 20); // result: u32 // Add a Nil (unit) requirement let fx = Fx::immediate(21u32, 42u32); let fx2 = fx.and_nil::<(u32, ())>(); let result = fx2.provide((21, ())).eval(); assert_eq!(result, 42); // Swap requirements let fx = Fx::immediate((5u8, 10u16), 15u16); let fx2 = fx.and_swap::<u8, u16, (u16, u8)>(); let result = fx2.provide((10u16, 5u8)).eval(); assert_eq!(result, 15); // Nest and flatten requirements let fx = Fx::immediate((3u8, 7u16), 21u16); let nested = fx.and_nest::<u8, u16>(); let inner = nested.provide(3u8).eval(); let result = inner.provide(7u16).eval(); assert_eq!(result, 21); let flat = fx.and_nest::<u8, u16>().and_flat::<(u8, u16)>(); let result2 = flat.provide((4u8, 6u16)).eval(); assert_eq!(result2, 21); }
Providing Requirements
Effect requirements can be provided in any order, with no impact on program evaluation.
Provide Combinators
These functions are used to eliminate requirements from effects. Only when ()
is the only remaining requirement can the effect be evaluated.
#![allow(unused)] fn main() { use fx::Fx; // Type alias for a requirement-providing effect type TimesTen = Fx<usize, usize>; let fx: TimesTen = Fx::pending(|n: usize| Fx::value(n * 10)); let fx2 = fx.provide(12); assert_eq!(fx2.eval(), 120); // result: usize // Type alias for a pair requirement type PairReq = Fx<(i32, i32), i32>; let fx: PairReq = Fx::value(7); let fx2 = fx.provide_left::<i32, i32>(1); // ... }
See the fx-rs API for more combinators and details.
Context Requirements
The State::get()
function creates an effect that requires some value S
as part of the environment and evaluates to S
.
It can be used to request the presence of services (traits or collections of methods that produce related effects) or, more generally, evidence that a value is part of the environment.
For example, mapping over a string to compute its length:
#![allow(unused)] fn main() { use fx::State; // Type alias for requirement effect type StringLen = Fx<String, usize>; let eff: StringLen = State::get::<String>().map(|s| s.len()); let result: usize = eff.provide("hello".to_owned()).eval(); assert_eq!(result, 5); // result: usize }
Calling Fx::value
is like mapping a context over a constant function. Fx::pure
is defined in terms of Fx::value
.
#![allow(unused)] fn main() { use fx::State; use fx::Fx; let a = State::get::<String>().map(|_| 42); let b = Fx::value(42); let c = Fx::pure(22); }
Handlers
A Handler is an effect transformation function of type impl Handler<'f, R, S, U, V>
(see the Handler
trait in fx-rs).
Handlers can change effect requirements, typically reducing them, but may also introduce new requirements or change the result type.
Handling an Effect
Let's rewrite the "length of string" function as a handler in Rust:
#![allow(unused)] fn main() { use fx::Fx; use fx::Handler; // Type alias for handler effect type RWHandler = Fx<Env, Output>; // Handler that takes Fx<(LenFn, ()), usize> and returns Fx<(), usize> let len_handler: RWHandler = |fx: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize>| { fx.provide_left(|s: String| Fx::pure(s.len())) }; let effect: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize> = Fx::func(|s: String| Fx::pure(s.len())); let handled: Fx<'static, (), usize> = len_handler.handle(effect); let result = handled.eval(); assert_eq!(result, "hello".len()); }
Requesting Handlers from the Environment
You can also request that a handler be present as a requirement. This way, the handler is provided once and can be applied anywhere in the program.
#![allow(unused)] fn main() { use fx::Fx; use fx::Handler; // Type alias for handler effect type RWHandler = Fx<Env, Output>; let len_handler: RWHandler = |fx: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize>| { fx.provide_left(|s: String| Fx::pure(s.len())) }; let effect: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize> = Fx::func(|s: String| Fx::pure(s.len())); let provided = effect.provide_left(len_handler); let result = provided.eval(); assert_eq!(result, "hello".len()); }
Handlers in fx-rs are just values and can be passed, composed, or swapped as needed.
A Handler in fx-rs is a transformation: it takes an input (often an effectful request or ability) and produces a new effect. Conceptually, a handler is a function that interprets or transforms effects, often by providing implementations for abilities or by composing/rewriting effects. See the comment in handler.rs
for details.
An Ability is a trait or type that represents a capability or effectful operation. In fx-rs, an ability is conceptually a function of the form I => Fx<S, O>
, meaning it takes an input I
and returns an effectful computation producing an output O
and possibly requiring further abilities S
. See the comment in ability.rs
for the canonical definition.
Reader/Writer
A Reader<T>
allows reading values of type T
from the environment, while Writer<T>
allows setting them.
Read and Write handlers take an effectful operation that can modify the external world. See the fx-rs codebase and tests for usage examples.
Abort
Abort<V, E>(E)
aborts a computation of type V
with an error E
.
When aborted, a computation is halted: any map
or flat_map
operation over it will not be executed.
The AbortHandler
replaces aborted effects (where Abort
was called) with a Result<V, E>
. If the effect was never aborted, a Result<V, E>
is returned.
See the implementation and tests in the fx-rs codebase for details.
State Effects, Lenses, and Contexts in fx-rs
fx-rs provides modular and composable state effects, with first-class support for lenses, struct-based contexts, and advanced combinators for working with state and context.
Modular State and Lenses
State effects are not global; you can focus on any part of your context using lenses. This allows you to write effectful code that only depends on the state it needs, improving modularity and testability.
Example: Using a Lens
#![allow(unused)] fn main() { use fx::{State, Lens}; // Type alias for state lens effect type LensEff = Fx<State, Value>; #[derive(Clone)] struct AppState { counter: u32, user: User, } #[derive(Clone)] struct User { name: String, } let lens = Lens::<AppState, u32>::new(); let get_counter = State::<AppState>::get().via(lens.zoom_out()); }
Struct-Based Contexts and Pair
fx-rs supports both tuple and struct-based contexts. The Pair
trait allows you to use custom structs as effect environments, not just tuples.
Example: Pair for Struct Context
#![allow(unused)] fn main() { use fx::Pair; #[derive(Clone)] struct Ctx { a: i32, b: bool } impl Pair<i32, bool> for Ctx { fn fst(self) -> i32 { self.a } fn snd(self) -> bool { self.b } } }
This enables ergonomic, named, and type-safe contexts for your effects.
forall: Quantified Effects
The forall
combinator allows you to abstract over part of the context, making your effect generic over some state or ability.
Example: forall
#![allow(unused)] fn main() { use fx::{Fx, State}; let fx: Fx<'static, i32, i32> = State::<i32>::get(); let fx2 = fx.forall(|fx| fx.map(|n| n + 1)); let result = fx2.provide(41).eval(); assert_eq!(result, 42); }
provide_part: Partial Context Provision
provide_part
allows you to provide only part of a context, leaving the rest to be provided later. This is useful for composing effects with partially known environments.
Example: provide_part
#![allow(unused)] fn main() { use fx::Fx; let fx: Fx<'static, (i32, bool), i32> = Fx::value(7); let fx2 = fx.provide_part::<i32, bool>(1); // Now Fx<'static, bool, i32> }
field_macro: Field-Based Lenses
The field_macro
crate provides macros to automatically generate lenses for struct fields, making it easy to focus on and update nested fields in your state.
Example: field_macro
#![allow(unused)] fn main() { use field_macro::Field; #[derive(Field, Clone)] struct AppState { #[field] counter: u32, #[field] user: User, } // Now you can use AppState::counter() and AppState::user() as lenses. }
Lenses, struct-based contexts, and advanced combinators make fx-rs a powerful tool for modular, reusable, and testable stateful code.
First-Class Environments and Context Structs
Contexts in fx-rs are just Rust structs, supporting named, nested, and multiple abilities.
Example: Composing Contexts
#![allow(unused)] fn main() { use fx::Fx; struct Logger; struct HttpClient; struct AppContext { logger: Logger, http: HttpClient } fn log_and_fetch<'f>(ctx: &'f AppContext) -> Fx<'f, AppContext, ()> { Fx::pure(move |c: &AppContext| { // Use c.logger and c.http }) } }
This makes dependency management explicit, readable, and maintainable.
Dependency Injection and Programmable Typeclasses
fx-rs unifies algebraic effects and dependency injection. Handlers are first-class values, not static typeclasses, so you can swap them at runtime and scope them to subcomputations.
Example: Swapping Handlers for a Subcomputation
#![allow(unused)] fn main() { use fx::Fx; trait Logger { fn log(&self, msg: &str); } struct ProdLogger; impl Logger for ProdLogger { fn log(&self, msg: &str) { println!("prod: {}", msg); } } struct TestLogger; impl Logger for TestLogger { fn log(&self, msg: &str) { println!("test: {}", msg); } } struct AppContext<L: Logger> { logger: L } fn log_something<'f, L: Logger>(msg: &'f str) -> Fx<'f, L, ()> { Fx::pure(move |l: &L| l.log(msg)) } let prod_ctx = AppContext { logger: ProdLogger }; let fx = log_something("main"); let _ = fx.run(&prod_ctx.logger); // Swap to a test logger for a subcomputation let test_ctx = AppContext { logger: TestLogger }; let _ = fx.adapt(|_| &test_ctx.logger, |c, r| (c, r)).run(&prod_ctx.logger); }
This enables modular, testable, and flexible dependency management—without global singletons or static typeclasses.
Direct-Style and Builder APIs
fx-rs supports not just monadic chaining but also direct-style macros and builder patterns for ergonomic effectful code.
Example: Direct-Style Macro with fx_do!
The fx_do!
macro allows you to write effectful code in a direct, imperative style. Under the hood, .same()
is used for map_m
(monadic map), and .bind()
is used for flat_map
(monadic bind):
#![allow(unused)] fn main() { use fx::Fx; use fx_do::fx_do; fx_do! { let x = Fx::pure(1); let y = x.same(); // equivalent to .map_m let z = y.bind(); // equivalent to .flat_map Fx::pure(z) } }
Example: Builder Pattern
#![allow(unused)] fn main() { use fx::Fx; let result = Fx::builder() .get_value() .compute() .log() .run(); }
These patterns reduce boilerplate and make effectful code look and feel like regular Rust.
Macro-Based Ergonomics in fx-rs
fx-rs uses Rust's macro system to provide ergonomic APIs and reduce boilerplate for effectful programming. Some highlights:
Ability Derivation
Procedural macros in the abilities_macro
crate allow you to derive ability traits and handlers automatically, making it easy to define new effectful operations and their interpreters.
Direct-Style Macros
The do_macro
and do_traits
crates provide macros for writing effectful code in a direct, imperative style, reducing the need for manual chaining and improving readability.
Example: fx_do! with .same() and .bind()
The fx_do!
macro allows you to write effectful code in a direct style. Under the hood, .same()
is used for map_m
(monadic map), and .bind()
is used for flat_map
(monadic bind):
#![allow(unused)] fn main() { use fx::Fx; use fx_do::fx_do; // Type alias for macro-based effect type MacroEff = Fx<MacroEnv, MacroVal>; fx_do! { let x: MacroEff = Fx::pure(1); let y = x.same(); // equivalent to .map_m let z = y.bind(); // equivalent to .flat_map Fx::pure(z) } }
Lens and Field Macros
The lens_macro
, field_macro
, and forall_macro
crates provide macros for generating lenses and working with generic fields, enabling fine-grained, type-safe state manipulation and modular effect composition.
field_macro: Field-Based Lenses
The field_macro
crate provides macros to automatically generate lenses for struct fields, making it easy to focus on and update nested fields in your state.
Example: field_macro
#![allow(unused)] fn main() { use field_macro::Field; #[derive(Field, Clone)] struct AppState { #[field] counter: u32, #[field] user: User, } // Now you can use AppState::counter() and AppState::user() as lenses. }
forall_macro: Quantified Field Access
The forall_macro
crate provides macros for working with generic fields and quantified access, enabling advanced patterns for generic and reusable effectful code.
ContextBuilder Macro: Effectful Context Provision
The builder_macro
crate provides a procedural macro for generating type-safe, incremental context builders for struct contexts. This enables ergonomic and effectful context provision, especially when working with fx-rs's effect system and trait-based dependency injection.
Example: Using ContextBuilder for Effectful Contexts
#![allow(unused)] fn main() { use builder_macro::ContextBuilder; use fx::{Has, Put}; #[derive(ContextBuilder, Debug, Clone)] struct MyContext { a: i32, b: String, c: bool, } // Incrementally build the context let ctx = MyContextBuilder::empty() .a(42) .b("hello".to_string()) .c(true) .build(); // Use trait-based access assert_eq!(Has::<i32>::get(ctx.clone()), 42); assert_eq!(Has::<String>::get(ctx.clone()), "hello"); assert_eq!(Has::<bool>::get(ctx.clone()), true); // Use macro-generated accessors assert_eq!(ctx.a(), Some(42)); assert!(ctx.has_a()); // Compose with fx-rs effect system // (see fx.rs book for more advanced examples) }
The macro generates:
- A builder type with
put_*
methods for each field - Marker types for tracking field presence
build()
method (requires all fields set)- Trait impls for
Has
andPut
- Field accessors and presence checks
This pattern enables ergonomic, type-safe context provision and mutation for effectful code.
These macros are designed to work seamlessly with the fx-rs core, making advanced effect patterns accessible and ergonomic for Rust developers.
Advanced Handler Composition
Effect Polymorphism and Generic Abstractions
fx-rs enables writing code generic over effects, supporting reusable libraries and higher-order effectful functions.
Example: Generic Logging
#![allow(unused)] fn main() { use fx::Fx; trait Log { fn log(&self, msg: &str); } fn with_logging<'f, L: Log>(msg: &'f str) -> Fx<'f, L, ()> { Fx::pure(move |l: &L| l.log(msg)) } // Can be used with any Log implementation }
This enables scalable, composable effectful code and reusable libraries.
Structured Concurrency in fx-rs
fx-rs models concurrency primitives (spawning, channels, async tasks) as abilities. This makes concurrent operations explicit, composable, and testable.
Composable Concurrency
You can add concurrency abilities to your context, and write effectful code that requests concurrency without knowing the underlying executor. Handlers can swap between real and mock concurrency backends for robust testing.
Example
#![allow(unused)] fn main() { trait Spawn { fn spawn<Fut: Future<Output = ()> + Send + 'static>(&self, fut: Fut); } // Add Spawn to your context and use it in effectful code. }
This approach enables structured concurrency, deterministic testing, and modular integration with Rust's async ecosystem.
Performance and Trade-offs
fx-rs is designed for flexibility and composability, but also aims for competitive performance. It uses dynamic dispatch for handler values, but leverages Rust's inlining and monomorphization where possible.
Example: Static vs Dynamic Dispatch
#![allow(unused)] fn main() { use fx::Fx; fn static_add(n: u32) -> u32 { n + 1 } let fx_static = Fx::func(static_add); let result = fx_static.provide(41).eval(); assert_eq!(result, 42); let fx_dyn = Fx::pending(|n: u32| Fx::value(n + 1)); let result = fx_dyn.provide(41).eval(); assert_eq!(result, 42); }
Use static dispatch for performance-critical code, and dynamic handler values for flexibility and modularity.
Resource Management and Bracket Patterns
fx-rs supports safe, composable resource management using bracket-like APIs and Rust’s ownership model.
Example: File Handling
#![allow(unused)] fn main() { use fx::Fx; fn with_file<'f>(path: &'f str) -> Fx<'f, (), String> { Fx::pure(move |_| format!("opened {}", path)) } let fx = with_file("foo.txt"); let result = fx.eval(); assert_eq!(result, "opened foo.txt"); }
Resources are acquired, used, and released safely, with automatic cleanup and error handling.
Testing Effectful Code
fx-rs makes it easy to test effectful code by swapping in test handlers or mocks.
Example: Mocking an Ability
#![allow(unused)] fn main() { use fx::Fx; trait Http { fn get(&self, url: &str) -> String; } struct MockHttp; impl Http for MockHttp { fn get(&self, url: &str) -> String { format!("mocked: {}", url) } } fn fetch<'f, H: Http>(url: &'f str) -> Fx<'f, H, String> { Fx::pure(move |h: &H| h.get(url)) } let fx = fetch("/test"); let result = fx.run(&MockHttp); assert_eq!(result, "mocked: /test"); }
You can also capture outputs, use property-based testing, and swap handlers for deterministic tests.
What Makes fx-rs Unique
fx-rs is not just another effect system ported to Rust. It is designed from the ground up to take advantage of Rust's type system, ownership, and module system, while providing a modern, ergonomic, and composable effect API. Here are some of the features that make fx-rs stand out:
1. Programmable Handlers as First-Class Values
Handlers in fx-rs are values, not static typeclasses. This means you can pass, compose, and swap handlers at runtime, enabling scoped and modular effect interpretation. Handler replacement is explicit and type-safe, allowing for powerful patterns like test mocking, dependency injection, and context adaptation.
2. Trait-Based Abilities and Contexts
Abilities are defined as Rust traits, and the effect context is any Rust struct. This enables named, nested, and multiple abilities, and makes dependency management explicit and ergonomic. You can use Rust's trait system to define effectful operations, and context structs to group and manage abilities.
3. Composable Context Adaptation
fx-rs provides combinators like adapt
and contra_map
to adapt computations to different contexts. This allows for scoped dependency replacement, modular handler wiring, and seamless integration with existing Rust code.
4. Direct-Style and Builder APIs
While fx-rs supports traditional map
/flat_map
combinators, it also offers (and is evolving) direct-style macros and builder patterns for more ergonomic effectful code. This reduces boilerplate and makes effectful code look and feel like regular Rust.
5. Lenses and Fine-Grained State
State effects in fx-rs are modular and composable, with support for lenses to focus on subparts of state. This enables fine-grained, type-safe state manipulation and modular effect composition.
6. Structured Concurrency as Effects
Concurrency primitives (spawning, channels, async tasks) can be modeled as abilities, making concurrent operations explicit, composable, and testable. Handlers can swap between real and mock concurrency backends for robust testing.
7. Macro-Based Ergonomics
fx-rs leverages Rust's macro system (see the macro crates in the repository) to reduce boilerplate, derive abilities, and provide ergonomic APIs for effectful programming.
Future Work & Research Directions
fx-rs is designed to be extensible and to inspire further research and development in effect systems for Rust. Some promising directions and open questions include:
-
Modular Reasoning & Scoped Handlers:
- Advanced patterns for modular, component-based architectures using fx-rs, where each module declares its effect signature and dependencies as traits.
- Scoped handler replacement (
Fx::adapt
) for targeted testing, feature flags, and environment-specific behavior. - Building up application context from smaller module contexts for scalable composition.
-
Effect Row Inference and Minimization:
- Static and dynamic effect row inference for scalable effect systems.
- Linter and IDE integration for effect signature inference and minimization.
- Exploring trade-offs between manual and inferred effect signatures.
-
Security and Capability Effects:
- Modeling permissions and capabilities as effects/abilities.
- Compile-time enforcement of security policies via capabilities.
- Patterns for capability injection, fine-grained permission checks, and compile-time security.
-
First-Class, Type-Safe Effect Scopes:
- Local reasoning and effect masking (scoping effects to regions).
- Encoding effect scopes in Rust's type system.
- Lexical vs. dynamic scoping for effects.
-
Resumable and Multi-Shot Handlers:
- Support for resumable exceptions, continuations, and multi-shot handlers.
- Encoding resumable/multi-shot handlers in Rust and exploring their use cases.
If you are interested in contributing to any of these areas, or have ideas for new features, please open an issue or discussion on the fx-rs repository!