Introduction

An Algebraic Effects System for Rust.

fx-go

Fx.rs is currently experimental.

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 type Fx<S, V> from a function f: S -> V.
  • .provide(value) discharges the requirement and returns Fx<(), 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 Anded 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 Anded 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 and Put
  • 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!