Algebraic Effects System for Rust

WIP: book (#12)

authored by oeiuwq.com and committed by

GitHub 35bcb90b 805b7e9f

+674 -434
+25 -15
book/src/SUMMARY.md
··· 1 1 # Summary 2 2 3 - - [Introduction](./introduction.md) 4 - - [Concepts Tour](./concepts.md) 5 - - [Basic Effects](./basics.md) 6 - - [Effect Requirements](./requirements.md) 7 - - [Shuffling Ands](./requirements/shuffling.md) 8 - - [Providers](./requirements/providers.md) 9 - - [Context Requirements](./requirements/context.md) 10 - - [Functional Requirements](./requirements/apply.md) 11 - - [Effect Requests](./requests.md) 12 - - [Handlers](./handlers.md) 13 - - [Example HTTP app](./http.md) 14 - - [Core Effects](./core.md) 15 - - [Reader/Writer](./rw.md) 16 - - [Abort](./abort.md) 17 - - [Contributing](./contributing.md) 3 + - [Introduction](introduction.md) 4 + - [Concepts](concepts.md) 5 + - [Basics](basics.md) 6 + - [Core Effects](core.md) 7 + - [Effect Requirements](requirements.md) 8 + - [Applying Requirements](req_apply.md) 9 + - [Shuffling Requirements](req_shuffling.md) 10 + - [Providing Requirements](req_providers.md) 11 + - [Context Requirements](req_context.md) 12 + - [Handlers](handlers.md) 13 + - [Reader/Writer](rw.md) 14 + - [Abort](abort.md) 15 + - [State Effects and Lenses](state_lenses.md) 16 + - [First-Class Environments and Context Structs](environments.md) 17 + - [Dependency Injection and Programmable Typeclasses](di_typeclasses.md) 18 + - [Direct-Style and Builder APIs](direct_style.md) 19 + - [Macro-Based Ergonomics](macros.md) 20 + - [Advanced Handler Composition](handler_composition.md) 21 + - [Effect Polymorphism and Generic Abstractions](polymorphism.md) 22 + - [Structured Concurrency](concurrency.md) 23 + - [Performance and Trade-offs](performance.md) 24 + - [Resource Management and Bracket Patterns](resources.md) 25 + - [Testing Effectful Code](testing.md) 26 + - [What Makes fx-rs Interesting](uniqueness.md) 27 + - [Future Work & Research Directions](future_work.md)
+4 -7
book/src/abort.md
··· 1 1 # Abort 2 2 3 - `Abort[V, E](E)` aborts a computation of `V` with `E`. 3 + `Abort<V, E>(E)` aborts a computation of type `V` with an error `E`. 4 4 5 - When aborted, a computation is *halted*, meaning that any `Map` or `FlatMap` operation over it will not be executed. 5 + When aborted, a computation is halted: any `map` or `flat_map` operation over it will not be executed. 6 6 7 - The `AbortHandler` replaces aborted effects (those computations where Abort was called) with a `Result` with `E`. Otherwise if the effect was never aborted, a `Result` with `V` is returned. 7 + 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. 8 8 9 - - Implementation 10 - - [abort.go](https://github.com/vic/fx.go/blob/main/abort/abort.go) 11 - - [result.go](https://github.com/vic/fx.go/blob/main/abort/result.go) 12 - - Tests [abort_test.go](https://github.com/vic/fx.go/blob/main/abort/abort_test.go) 9 + See the implementation and tests in the fx-rs codebase for details.
+33 -36
book/src/basics.md
··· 1 - # Basic Effects on `Fx.go` 1 + # Basic Effects in `fx-rs` 2 2 3 - This section expands on [Concepts](concepts.html) and shows how they relate to the `Fx.go` API, as well as providing a basic intuition on Effect requirements and evaluation. 3 + This section expands on [Concepts](concepts.html) and shows how they relate to the `fx-rs` API, providing intuition on effect requirements and evaluation in Rust. 4 4 5 - ## `Pure[V]`: Immediate Effects 5 + ## `Fx<(), V>`: Immediate Effects 6 6 7 - The most basic of all possible effects are *Immediate* effects. These are effects of type `Fx[Nil, V]`, meaning that they have no ability requirements (`Nil`) and evaluate to a value (`V`). 7 + 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`. 8 8 9 - Immediate effects are created using the `Pure(V) Fx[Nil, V]` function. 9 + Immediate effects are created using `Fx::pure(value)` or `Fx::value(value)`. 10 + 11 + A pure effect just holds an already known value instead of computing it. 10 12 11 - As you can see, `Pure(V)` takes an *existing* value `V`, that means that a pure effect just holds an already known value instead of trying to compute it. 13 + The value can be retrieved by calling `.eval()` on the effect. Only effects with no requirements (`()`) can be evaluated directly. 12 14 13 - The value given to `Pure` can be retrieved by using `Eval(Fx[Nil, V]) V`. Only effects that have no requirements (`Nil`) can be evaluated. 15 + ```rust 16 + use fx::Fx; 14 17 15 - ```go 16 - import ( fx "github.com/vic/fx.go" ) 18 + // Type alias for a simple effect with no requirements 19 + type PureString = Fx<(), String>; 17 20 18 - func PureExample() { 19 - v := "Hello World" 20 - // Code annotated with types for clarity 21 - var effect fx.Fx[fx.Nil, string] = fx.Pure(v) 22 - var result string = fx.Eval(effect) 23 - assert(result == v) 21 + let v = "Hello World".to_owned(); 22 + let effect: PureString = Fx::pure(v.clone()); 23 + let result: String = effect.eval(); 24 + assert_eq!(result, v); // result: String 24 25 } 25 26 ``` 26 27 27 - ## `Fx[S, V]`: Pending effects 28 + ## `Fx<S, V>`: Pending Effects 28 29 29 - An effect `Fx[S, V]` where `S != Nil` is a pending effect that needs `S` to be _provided_ for computing `V`. 30 + An effect `Fx<S, V>` where `S` is not `()` is a pending effect that needs `S` to be provided before computing `V`. 30 31 31 - The most basic pending computation is one you are already familiar with: *A function*. 32 + 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>`: 32 33 33 - In the following example, the function `LengthOfString(string) int` can be expressed as an Effect of type `Fx[string, int]`. Meaning that in order to have a value of `int` you need first to provide an `string` value: 34 + ```rust 35 + use fx::Fx; 34 36 35 - ```go 36 - func LengthOfString(s string) int { 37 - return len(s) 37 + fn length_of_string(s: String) -> usize { 38 + s.len() 38 39 } 39 40 40 - func FuncExample() { 41 - // Code annotated with types for clarity 42 - var effect fx.Fx[string, int] = fx.Func(LengthOfString) 43 - var requirement string = "Hello World" 44 - var provided fx.Fx[fx.Nil, int] = fx.Provide(effect, requirement) 45 - var result int = fx.Eval(provided) 46 - assert(result == len(requirement)) 41 + fn func_example() { 42 + let effect: Fx<String, usize> = Fx::func(length_of_string); 43 + let requirement = "Hello World".to_owned(); 44 + let provided = effect.provide(requirement.clone()); 45 + let result = provided.eval(); 46 + assert_eq!(result, requirement.len()); 47 47 } 48 48 ``` 49 49 50 - From the code above: 50 + - `Fx::func(f)` produces a pending effect of type `Fx<S, V>` from a function `f: S -> V`. 51 + - `.provide(value)` discharges the requirement and returns `Fx<(), V>`. No computation is performed until `.eval()` is called. 52 + - `.eval()` performs the computation, since all requirements have been provided. 51 53 52 - - `Func(func (S) V)` produces a _pending_ effect of type `Fx[S, V]`. 53 - - `Provide(Fx[S, V], S)` discharges the `S` requirement and returns `Fx[Nil, V]`.\ 54 - Note that *no computation* is performed in this step. `Fx[Nil, V]` is still a description of a program, and `V` has not been computed yet, nor any side-effect has been performed. 55 - - `Eval(Fx[Nil, V])` will actually perform the computation of `V`. Since all `non-Nil` requirements have already been provided, the computation can be run. 56 - 57 - These two are the most basic effects in `Fx.go`. More interesting effects will be presented as we explore the topics of effect Rquests and Handlers. 54 + These are the most basic effects in `fx-rs`. More interesting effects are presented in later chapters.
+17 -20
book/src/concepts.md
··· 1 1 # Concepts Tour 2 2 3 - This section will try to introduce you to the concepts of 4 - _Effects_, _Abilities_ and _Handlers_ as present in `Fx.go`. 3 + This section introduces the concepts of _Effects_, _Abilities_, and _Handlers_ as present in `fx-rs`. 5 4 6 - No knowledge or previous experience with other effect sytems 7 - is expected. We will try to explain things by 8 - working out from simple concepts to more interesting ones. 5 + No prior experience with other effect systems is required. We explain concepts from simple to advanced, using Rust idioms. 9 6 10 7 ## Effects 11 8 12 - An *Effect* ( `Fx[S, V]` read: `V` provided `S` ) is the _description_ of a program that computes `V`, *provided* that the requirement `S` is present, so that the computation of `V` can be performed. 9 + 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. 13 10 14 - Since effects are *description*s of programs, they compute nothing nor produce side-effects until they are finally evaluated, once all their requirements are at met. 11 + Effects are descriptions of programs; they compute nothing and produce no side-effects until they are evaluated, once all requirements are met. 15 12 16 - Some people also use the *recipe* analogy for effects: you first have a precise description of each step it takes to cook something, along with a list of the requirements for it (the ingredients and utencils you will use) and once you have them all, you can actually perform the recipe. 13 + 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. 17 14 18 15 ## Abilities 19 16 20 - In `Fx[S, V]`, `S` is said to be the *Ability* (sometimes also referred as the _set_ of *Abilities*, *Capabilities*, *Effect Environment* or *Effect Requirements*) that are needed for computing `V`. 17 + In `Fx<S, V>`, `S` is the *Ability* (sometimes called the set of Abilities, Capabilities, or Effect Environment) needed to compute `V`. 21 18 22 - Abilities describe the external resources that would be needed, as well as the _side-effects_ that are possible while computing `V`. 19 + Abilities describe the external resources or side-effects possible while computing `V`. 23 20 24 - Examples of such Abilities are: 21 + Examples: 25 22 26 - - network abilities (eg, performing http requests) 27 - - console abilities (eg, printing to the terminal or reading user input) 28 - - non-deterministic abilities (eg, generating random numbers or coin-flips) 29 - - resource handling (eg, disciplined acquire/use/release of shared/limited resources) 30 - - exception handling (eg, interruption/resumption and finalizers) 31 - - anything else that interacts with the world outside of the program. 23 + - network abilities (e.g., HTTP requests) 24 + - console abilities (e.g., printing, reading input) 25 + - non-deterministic abilities (e.g., random numbers) 26 + - resource handling (e.g., managing shared resources) 27 + - exception handling (e.g., interruption, finalizers) 28 + - anything else that interacts with the outside world 32 29 33 30 ## Handlers 34 31 35 - A *Handler* for the `S` ability is a particular _interpretation_ of what `S` means. 32 + A *Handler* for an ability is a particular _interpretation_ of what that ability means. 36 33 37 - Handlers are the only _side-effectful_ portion of your programs. It is possible, and quite common, to have different handlers (interpretations) for the same Ability, and each Handler decides _how/when_ to perform world-modifying _side-effects_. 34 + 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. 38 35 39 - For example, for an _http-request_ ability you can have a *test-handler* that just mock responses to fixed values so that you can easily assert on known values on your tests. You could also have a *live-handler* that actually performs requests via the network for production runs. 36 + 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.
+21
book/src/concurrency.md
··· 1 + # Structured Concurrency in fx-rs 2 + 3 + fx-rs models concurrency primitives (spawning, channels, async tasks) as abilities. This makes concurrent operations explicit, composable, and testable. 4 + 5 + ## Composable Concurrency 6 + 7 + 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. 8 + 9 + ## Example 10 + 11 + ```rust 12 + trait Spawn { 13 + fn spawn<Fut: Future<Output = ()> + Send + 'static>(&self, fut: Fut); 14 + } 15 + 16 + // Add Spawn to your context and use it in effectful code. 17 + ``` 18 + 19 + ______________________________________________________________________ 20 + 21 + This approach enables structured concurrency, deterministic testing, and modular integration with Rust's async ecosystem.
-3
book/src/contributing.md
··· 1 - # Contributing 2 - 3 - Yes, please. All PRs are welcome, documentation, code improvements, ideas or suggestions, ways to improve this book prose, etc.
+18 -5
book/src/core.md
··· 1 1 # Core Effects 2 2 3 - The list of effects provided by `Fx.go` will increase as we see the need for them and as long as they provide useful on Golang programs. 3 + The list of effects provided by `fx-rs` will grow as new needs are discovered in Rust programs. 4 + 5 + Current ideas for contributions: 6 + 7 + - Resource management (safe acquire/use/release of resources) 8 + - Structured concurrency (beyond manual mutex/channels) 9 + - Any other effectful pattern useful in Rust—issues and PRs are welcome! 10 + 11 + ```rust 12 + // Type alias for a function effect 13 + type StringToUsize = Fx<String, usize>; 4 14 5 - Current Ideas for contributions: 15 + fn length_of_string(s: String) -> usize { s.len() } 6 16 7 - - Resource management (reserve/use/release shared resources) 8 - - Structured concurrency (instead of manually using mutex/channels) 9 - - any other idea you find useful, [Issues and PRs](https://github.com/vic/fx.go) are welcome! 17 + let effect: StringToUsize = Fx::func(length_of_string); 18 + let requirement = "Hello World".to_owned(); 19 + let provided = effect.provide(requirement.clone()); 20 + let result: usize = provided.eval(); 21 + assert_eq!(result, requirement.len()); // result: usize 22 + ```
+31
book/src/di_typeclasses.md
··· 1 + # Dependency Injection and Programmable Typeclasses 2 + 3 + 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. 4 + 5 + ## Example: Swapping Handlers for a Subcomputation 6 + 7 + ```rust 8 + use fx::Fx; 9 + 10 + trait Logger { fn log(&self, msg: &str); } 11 + struct ProdLogger; 12 + impl Logger for ProdLogger { fn log(&self, msg: &str) { println!("prod: {}", msg); } } 13 + struct TestLogger; 14 + impl Logger for TestLogger { fn log(&self, msg: &str) { println!("test: {}", msg); } } 15 + 16 + struct AppContext<L: Logger> { logger: L } 17 + 18 + fn log_something<'f, L: Logger>(msg: &'f str) -> Fx<'f, L, ()> { 19 + Fx::pure(move |l: &L| l.log(msg)) 20 + } 21 + 22 + let prod_ctx = AppContext { logger: ProdLogger }; 23 + let fx = log_something("main"); 24 + let _ = fx.run(&prod_ctx.logger); 25 + 26 + // Swap to a test logger for a subcomputation 27 + let test_ctx = AppContext { logger: TestLogger }; 28 + let _ = fx.adapt(|_| &test_ctx.logger, |c, r| (c, r)).run(&prod_ctx.logger); 29 + ``` 30 + 31 + This enables modular, testable, and flexible dependency management—without global singletons or static typeclasses.
+33
book/src/direct_style.md
··· 1 + # Direct-Style and Builder APIs 2 + 3 + fx-rs supports not just monadic chaining but also direct-style macros and builder patterns for ergonomic effectful code. 4 + 5 + ## Example: Direct-Style Macro with fx_do! 6 + 7 + 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): 8 + 9 + ```rust 10 + use fx::Fx; 11 + use fx_do::fx_do; 12 + 13 + fx_do! { 14 + let x = Fx::pure(1); 15 + let y = x.same(); // equivalent to .map_m 16 + let z = y.bind(); // equivalent to .flat_map 17 + Fx::pure(z) 18 + } 19 + ``` 20 + 21 + ## Example: Builder Pattern 22 + 23 + ```rust 24 + use fx::Fx; 25 + 26 + let result = Fx::builder() 27 + .get_value() 28 + .compute() 29 + .log() 30 + .run(); 31 + ``` 32 + 33 + These patterns reduce boilerplate and make effectful code look and feel like regular Rust.
+21
book/src/environments.md
··· 1 + # First-Class Environments and Context Structs 2 + 3 + Contexts in fx-rs are just Rust structs, supporting named, nested, and multiple abilities. 4 + 5 + ## Example: Composing Contexts 6 + 7 + ```rust 8 + use fx::Fx; 9 + 10 + struct Logger; 11 + struct HttpClient; 12 + struct AppContext { logger: Logger, http: HttpClient } 13 + 14 + fn log_and_fetch<'f>(ctx: &'f AppContext) -> Fx<'f, AppContext, ()> { 15 + Fx::pure(move |c: &AppContext| { 16 + // Use c.logger and c.http 17 + }) 18 + } 19 + ``` 20 + 21 + This makes dependency management explicit, readable, and maintainable.
+34
book/src/future_work.md
··· 1 + # Future Work & Research Directions 2 + 3 + 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: 4 + 5 + - **Modular Reasoning & Scoped Handlers:** 6 + 7 + - Advanced patterns for modular, component-based architectures using fx-rs, where each module declares its effect signature and dependencies as traits. 8 + - Scoped handler replacement (`Fx::adapt`) for targeted testing, feature flags, and environment-specific behavior. 9 + - Building up application context from smaller module contexts for scalable composition. 10 + 11 + - **Effect Row Inference and Minimization:** 12 + 13 + - Static and dynamic effect row inference for scalable effect systems. 14 + - Linter and IDE integration for effect signature inference and minimization. 15 + - Exploring trade-offs between manual and inferred effect signatures. 16 + 17 + - **Security and Capability Effects:** 18 + 19 + - Modeling permissions and capabilities as effects/abilities. 20 + - Compile-time enforcement of security policies via capabilities. 21 + - Patterns for capability injection, fine-grained permission checks, and compile-time security. 22 + 23 + - **First-Class, Type-Safe Effect Scopes:** 24 + 25 + - Local reasoning and effect masking (scoping effects to regions). 26 + - Encoding effect scopes in Rust's type system. 27 + - Lexical vs. dynamic scoping for effects. 28 + 29 + - **Resumable and Multi-Shot Handlers:** 30 + 31 + - Support for resumable exceptions, continuations, and multi-shot handlers. 32 + - Encoding resumable/multi-shot handlers in Rust and exploring their use cases. 33 + 34 + 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!
+1
book/src/handler_composition.md
··· 1 + # Advanced Handler Composition
+35 -41
book/src/handlers.md
··· 1 1 # Handlers 2 2 3 - A _Handler_ is an effect transformation function of type `func(Fx[R, U]) Fx[S, V]`. 3 + A _Handler_ is an effect transformation function of type `impl Handler<'f, R, S, U, V>` (see the `Handler` trait in fx-rs). 4 4 5 - Handlers are free to change the effect requirements, they tipically reduce the requirement set, but they could also introduce new requirements. They can also change or keep the result type. 5 + Handlers can change effect requirements, typically reducing them, but may also introduce new requirements or change the result type. 6 6 7 - ## Handling an effect 7 + ## Handling an Effect 8 8 9 - Lets re-write our previous "length of string" function as a Handler. 9 + Let's rewrite the "length of string" function as a handler in Rust: 10 10 11 - ```go 12 - type LenFn func(string) Fx[Nil, int] 11 + ```rust 12 + use fx::Fx; 13 + use fx::Handler; 13 14 14 - // Code is type annotated for clarity 15 - var lenFx Fx[And[LenFn, Nil], int] = fx.Suspend[LenFn]("hello") 16 - 17 - // type of a handler. not needed but added for clarity. 18 - type LenHn func(Fx[And[LenFn, Nil], int]) Fx[Nil, int] 15 + // Type alias for handler effect 16 + type RWHandler = Fx<Env, Output>; 19 17 20 - var handler LenHn = fx.Handler(func(s string) fx.Fx[fx.Nil, int] { 21 - return fx.Pure(len(s)) 22 - }) 18 + // Handler that takes Fx<(LenFn, ()), usize> and returns Fx<(), usize> 19 + let len_handler: RWHandler = |fx: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize>| { 20 + fx.provide_left(|s: String| Fx::pure(s.len())) 21 + }; 23 22 24 - // apply the handler directly to lenFx 25 - var x *int = fx.Eval(handler(lenFx)) 26 - assert(*x == 5) 23 + let effect: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize> = Fx::func(|s: String| Fx::pure(s.len())); 24 + let handled: Fx<'static, (), usize> = len_handler.handle(effect); 25 + let result = handled.eval(); 26 + assert_eq!(result, "hello".len()); 27 27 ``` 28 28 29 - As you might guess, `fx.Handler` is just a wrapper for `ProvideLeft(Fx[And[Fn, S], O], Fn) Fx[S, O]` where `Fn = func(I) Fx[S, O]`, an request-effect function. 29 + ## Requesting Handlers from the Environment 30 30 31 - ## Requesting Handlers (effect-transformers) from the environment. 31 + 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. 32 32 33 - Of course, you can also request that a particular effect transformer (Handler) be present as a requirement of some computation. This way the handler is provided only once but can be applied anywhere it is needed inside the program. 33 + ```rust 34 + use fx::Fx; 35 + use fx::Handler; 34 36 35 - ```go 36 - // Same examples as above with some more types for clarity 37 + // Type alias for handler effect 38 + type RWHandler = Fx<Env, Output>; 37 39 38 - // effect-request function type. 39 - type LenFn func(string) Fx[Nil, int] 40 - // effect handler type 41 - type LenHn = func(Fx[And[LenFn, Nil], int]) Fx[Nil, int] 40 + let len_handler: RWHandler = |fx: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize>| { 41 + fx.provide_left(|s: String| Fx::pure(s.len())) 42 + }; 42 43 43 - // effect ability 44 - type LenAb = And[LenHn, Nil] 45 - // effect type producing V 46 - type LenFx[V any] = fx.Fx[LenAb, V] 44 + let effect: Fx<'static, (fn(String) -> Fx<'static, (), usize>, ()), usize> = Fx::func(|s: String| Fx::pure(s.len())); 45 + let provided = effect.provide_left(len_handler); 46 + let result = provided.eval(); 47 + assert_eq!(result, "hello".len()); 48 + ``` 47 49 48 - // Same as: Suspend[LenHn](Suspend[LenFn](input)) 49 - var lenFx LenFx = fx.Handle[LenHn]("hello") 50 + Handlers in fx-rs are just values and can be passed, composed, or swapped as needed. 50 51 51 - var handler LenHn = fx.Handler(func(s string) fx.Fx[fx.Nil, int] { 52 - return fx.Pure(len(s)) 53 - }) 52 + 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. 54 53 55 - // Now instead of applying the handler directly to each effect 56 - // we provide it into the environment. 57 - var provided Fx[Nil, int] = fx.ProvideLeft(lenFx, handler) 58 - val x int = fx.Eval(provided) 59 - assert(x == 5) 60 - ``` 54 + 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.
-62
book/src/http.md
··· 1 - # Example HTTP Req/Res program 2 - 3 - As an example of the concepts we have seen so far, lets write a program that needs to perform an HTTP Request, expects an HTTP response from the webservice it accesses and then perform some logic on that response. 4 - 5 - On an effect system like `Fx.go`, we *do not* directly contact external services, we just express our need to perform such requests and we expect a `Handler` to actually decide how and when such request should be performed (if any). 6 - 7 - ```go 8 - package http_example 9 - 10 - // Notice we do not import any HTTP library, just effects. 11 - import ( 12 - "testing" 13 - fx "github.com/vic/fx.go" 14 - ) 15 - 16 - // For simplicity our request is just an string: An URL. 17 - type HttpRq string 18 - 19 - // For simplicity out response is just an string: The response body. 20 - type HttpRs string 21 - 22 - // Type of the effect-request function. 23 - // This will be implemented by some handler to provide actual responses. 24 - // 25 - // fx.Nil in the result type indicates that our Http ability does 26 - // not requires any other ability. 27 - type HttpFn func(HttpRq) fx.Fx[fx.Nil, HttpRs] 28 - 29 - // Type of the Http Handler that discharges effect requirements 30 - type HttpHn = func(fx.Fx[fx.And[HttpFn, fx.Nil], HttpRs]) fx.Fx[fx.Nil, HttpRs] 31 - 32 - // Type of the Http Ability: the handler and aditional abilities. 33 - type HttpAb = fx.And[HttpHn, fx.Nil] 34 - 35 - // An http effect that produces V 36 - type HttpFx[V any] = fx.Fx[HttpAb, V] 37 - 38 - // An effect of HTTP GET requests. 39 - func Get(url HttpRq) HttpFx[HttpRs] { 40 - return fx.Handle[HttpHn](url) 41 - } 42 - 43 - // A program that computes the respose length of https://example.org 44 - func Program() HttpFx[int] { 45 - return fx.Map(Get("https://example.org"), func(r HttpRs) int { 46 - return len(r) 47 - }) 48 - } 49 - 50 - func TestProgram(t *testing.T) { 51 - var httpHandler HttpHn = fx.Handler(func(r HttpRq) fx.Fx[fx.Nil, HttpRs] { 52 - mock := HttpRs("example") 53 - return fx.Pure(&mock) 54 - }) 55 - var provided fx.Fx[fx.Nil, int] = fx.ProvideLeft(Program(), httpHandler) 56 - var result int = fx.Eval(provided) 57 - if result != len("example") { 58 - t.Errorf("Unexpected result %v", result) 59 - } 60 - } 61 - 62 - ```
+62
book/src/macros.md
··· 1 + # Macro-Based Ergonomics in fx-rs 2 + 3 + fx-rs uses Rust's macro system to provide ergonomic APIs and reduce boilerplate for effectful programming. Some highlights: 4 + 5 + ## Ability Derivation 6 + 7 + 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. 8 + 9 + ## Direct-Style Macros 10 + 11 + 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. 12 + 13 + ### Example: fx_do! with .same() and .bind() 14 + 15 + 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): 16 + 17 + ```rust 18 + use fx::Fx; 19 + use fx_do::fx_do; 20 + 21 + // Type alias for macro-based effect 22 + type MacroEff = Fx<MacroEnv, MacroVal>; 23 + 24 + fx_do! { 25 + let x: MacroEff = Fx::pure(1); 26 + let y = x.same(); // equivalent to .map_m 27 + let z = y.bind(); // equivalent to .flat_map 28 + Fx::pure(z) 29 + } 30 + ``` 31 + 32 + ## Lens and Field Macros 33 + 34 + 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. 35 + 36 + ### field_macro: Field-Based Lenses 37 + 38 + 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. 39 + 40 + #### Example: field_macro 41 + 42 + ```rust 43 + use field_macro::Field; 44 + 45 + #[derive(Field, Clone)] 46 + struct AppState { 47 + #[field] 48 + counter: u32, 49 + #[field] 50 + user: User, 51 + } 52 + 53 + // Now you can use AppState::counter() and AppState::user() as lenses. 54 + ``` 55 + 56 + ### forall_macro: Quantified Field Access 57 + 58 + The `forall_macro` crate provides macros for working with generic fields and quantified access, enabling advanced patterns for generic and reusable effectful code. 59 + 60 + ______________________________________________________________________ 61 + 62 + These macros are designed to work seamlessly with the fx-rs core, making advanced effect patterns accessible and ergonomic for Rust developers.
+20
book/src/performance.md
··· 1 + # Performance and Trade-offs 2 + 3 + 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. 4 + 5 + ## Example: Static vs Dynamic Dispatch 6 + 7 + ```rust 8 + use fx::Fx; 9 + 10 + fn static_add(n: u32) -> u32 { n + 1 } 11 + let fx_static = Fx::func(static_add); 12 + let result = fx_static.provide(41).eval(); 13 + assert_eq!(result, 42); 14 + 15 + let fx_dyn = Fx::pending(|n: u32| Fx::value(n + 1)); 16 + let result = fx_dyn.provide(41).eval(); 17 + assert_eq!(result, 42); 18 + ``` 19 + 20 + Use static dispatch for performance-critical code, and dynamic handler values for flexibility and modularity.
+19
book/src/polymorphism.md
··· 1 + # Effect Polymorphism and Generic Abstractions 2 + 3 + fx-rs enables writing code generic over effects, supporting reusable libraries and higher-order effectful functions. 4 + 5 + ## Example: Generic Logging 6 + 7 + ```rust 8 + use fx::Fx; 9 + 10 + trait Log { fn log(&self, msg: &str); } 11 + 12 + fn with_logging<'f, L: Log>(msg: &'f str) -> Fx<'f, L, ()> { 13 + Fx::pure(move |l: &L| l.log(msg)) 14 + } 15 + 16 + // Can be used with any Log implementation 17 + ``` 18 + 19 + This enables scalable, composable effectful code and reusable libraries.
+24
book/src/req_apply.md
··· 1 + # Functional Requirements 2 + 3 + As seen in the previous [Context](context.html) chapter, you can require any type to be part of an effect's environment. 4 + 5 + You can also require functions of some type to be present and use them in your program descriptions without knowing their exact implementation. 6 + 7 + For example, suppose we require a function from `usize` to `usize` and then apply it to a value: 8 + 9 + ```rust 10 + // Type alias for a function requirement 11 + type FnReq = Fx<fn(usize) -> usize, usize>; 12 + 13 + fn double(n: usize) -> usize { n * 2 } 14 + 15 + // The effect requires a function of type fn(usize) -> usize 16 + let effect: FnReq = Fx::require(); 17 + // Provide the function implementation as the requirement 18 + let applied = effect.provide(double); 19 + // Provide the input value to the function 20 + let result = applied.provide(12).eval(); 21 + assert_eq!(result, 24); // result: usize 22 + ``` 23 + 24 + For more complex cases, you can nest effect requests and flatten them using combinators, as in the fx-rs API.
+29
book/src/req_context.md
··· 1 + # Context Requirements 2 + 3 + The `State::get()` function creates an effect that requires some value `S` as part of the environment and evaluates to `S`. 4 + 5 + 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. 6 + 7 + For example, mapping over a string to compute its length: 8 + 9 + ```rust 10 + use fx::State; 11 + 12 + // Type alias for requirement effect 13 + type StringLen = Fx<String, usize>; 14 + 15 + let eff: StringLen = State::get::<String>().map(|s| s.len()); 16 + let result: usize = eff.provide("hello".to_owned()).eval(); 17 + assert_eq!(result, 5); // result: usize 18 + ``` 19 + 20 + Calling `Fx::value` is like mapping a context over a constant function. `Fx::pure` is defined in terms of `Fx::value`. 21 + 22 + ```rust 23 + use fx::State; 24 + use fx::Fx; 25 + 26 + let a = State::get::<String>().map(|_| 42); 27 + let b = Fx::value(42); 28 + let c = Fx::pure(22); 29 + ```
+27
book/src/req_providers.md
··· 1 + # Providing Requirements 2 + 3 + Effect requirements can be provided in any order, with no impact on program evaluation. 4 + 5 + ## Provide Combinators 6 + 7 + These functions are used to eliminate requirements from effects. Only when `()` is the only remaining requirement can the effect be evaluated. 8 + 9 + ```rust 10 + use fx::Fx; 11 + 12 + // Type alias for a requirement-providing effect 13 + type TimesTen = Fx<usize, usize>; 14 + 15 + let fx: TimesTen = Fx::pending(|n: usize| Fx::value(n * 10)); 16 + let fx2 = fx.provide(12); 17 + assert_eq!(fx2.eval(), 120); // result: usize 18 + 19 + // Type alias for a pair requirement 20 + type PairReq = Fx<(i32, i32), i32>; 21 + 22 + let fx: PairReq = Fx::value(7); 23 + let fx2 = fx.provide_left::<i32, i32>(1); 24 + // ... 25 + ``` 26 + 27 + See the fx-rs API for more combinators and details.
+41
book/src/req_shuffling.md
··· 1 + # Shuffling `And`ed Requirements 2 + 3 + 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. 4 + 5 + ## `And` Combinators 6 + 7 + Several combinators help you rearrange and manipulate `And`ed effect requirements: 8 + 9 + ```rust 10 + use fx::Fx; 11 + 12 + // Type alias for collapsed requirements 13 + type Collapsed = Fx<u32, u32>; 14 + 15 + let fx: Fx<(u32, u32), u32> = Fx::immediate((10u32, 10u32), 20u32); 16 + let fx2: Collapsed = fx.and_collapse::<u32>(); 17 + let result: u32 = fx2.provide(10).eval(); 18 + assert_eq!(result, 20); // result: u32 19 + 20 + // Add a Nil (unit) requirement 21 + let fx = Fx::immediate(21u32, 42u32); 22 + let fx2 = fx.and_nil::<(u32, ())>(); 23 + let result = fx2.provide((21, ())).eval(); 24 + assert_eq!(result, 42); 25 + 26 + // Swap requirements 27 + let fx = Fx::immediate((5u8, 10u16), 15u16); 28 + let fx2 = fx.and_swap::<u8, u16, (u16, u8)>(); 29 + let result = fx2.provide((10u16, 5u8)).eval(); 30 + assert_eq!(result, 15); 31 + 32 + // Nest and flatten requirements 33 + let fx = Fx::immediate((3u8, 7u16), 21u16); 34 + let nested = fx.and_nest::<u8, u16>(); 35 + let inner = nested.provide(3u8).eval(); 36 + let result = inner.provide(7u16).eval(); 37 + assert_eq!(result, 21); 38 + let flat = fx.and_nest::<u8, u16>().and_flat::<(u8, u16)>(); 39 + let result2 = flat.provide((4u8, 6u16)).eval(); 40 + assert_eq!(result2, 21); 41 + ```
-37
book/src/requests.md
··· 1 - # Effect Requests 2 - 3 - Another way of creating effects in `Fx.go` is via an *effect-request* function. 4 - 5 - A function of type `func(I) Fx[S, O]` is said to take an _effect-request_ `I` and produce an *suspended* effect `Fx[S, O]`. 6 - 7 - An example, is the function `func(HttpReq) Fx[HttpService, HttpRes]` we saw in the [previous](./requirements/apply.html) chapter. 8 - 9 - Using the "length of string" example of the previous chapters, we can use it to model an effect request: 10 - 11 - ```go 12 - type LenFn = func(string) fx.Fx[fx.Nil, int] 13 - 14 - // Code is type annotated for clarity 15 - var lenFx fx.Fx[fx.And[LenFn, fx.Nil], int] = fx.Suspend[LenFn]("hello") 16 - ``` 17 - 18 - Note that `Suspend` takes the _type_ of a request-effect function and the request value for it. And yields a *suspended* effect of type `Fx[And[LenFn, Nil], int]`. The computation is said to be *suspended* because it knows not what particular implementation of `LenFn` should be used, and because of this, `LenFn` is part of the requirements, along with `Nil` the ability requirement on the result of `LenFn`. 19 - 20 - Different implementations of `LenFn` can be provided to the `lenFx` effect. 21 - 22 - ```go 23 - var lies LenFn = func(_ string) fx.Fx[fx.Nil, int] { 24 - return fx.Pure(42) 25 - } 26 - var truth LenFn = func(s string) fx.Fx[fx.Nil, int] { 27 - return fx.Pure(len(s)) 28 - } 29 - 30 - var x int = fx.Eval(fx.ProvideLeft(lenFx, lies)) 31 - assert(x == 42) 32 - 33 - var y int = fx.Eval(fx.ProvideLeft(lenFx, truth)) 34 - assert(y == 5) 35 - ``` 36 - 37 - Notice that by delaying which implementation of `LenFn` is used, the `lenFx` program description includes the effect request `"hello"` and knows the general form of its response `Fx[Nil, int]`, but knows nothing about which particular interpretation of `LenFn` will be used.
+2 -59
book/src/requirements.md
··· 1 1 # Effect Requirements 2 2 3 - So far, we have seen that an effect `Fx[S, V]` can have `S` be `Nil` for effects that can be evaluated right away and non-`Nil` for those pending effects that still need to be provided some value. 4 - 5 - In this chapter we will talk about aggregating requirements using the `And` type. And how different types of functions/effects can represent the very same computation. We also look at similarities with higher-order functions in functional-programming and how rotating or re-arranging effect requirements is indifferent in `Fx.go`. Finally, we show some the`And*` and `Provide*` combinators that can help you reshape your effect requirements. 6 - 7 - ## Composite requirements 8 - 9 - Using the same "length of string" function from the previous chapters, we can describe it in different ways. 3 + 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. 10 4 11 - ```go 12 - // This is an strange way of writing `func(string) int`. 13 - // But this shape can help to understand the types bellow. 14 - // 15 - // Focus your attention on the requirements that are 16 - // needed for a computing a value. 17 - // 18 - // In particular, note that `Fx[Nil, V]` is like a `func() V` 19 - func LengthOfString(s string) func() int { 20 - return func() int { return len(s) } 21 - } 22 - 23 - // The type of LengthOfString expressed as an effect request. 24 - type LenFn = func(string) Fx[Nil, int] 25 - ``` 26 - 27 - Note that all of the following types are equivalent, as they describe the very same requirements and result types: 28 - 29 - - `func(string) int` 30 - - `Fx[string, int]` 31 - - `func(string) func() int` 32 - - `func(string) Fx[Nil, int]` 33 - - `Fx[string, Fx[Nil, int]]` 34 - - `Fx[And[string, Nil], int]` 35 - - `Fx[And[Nil, string], int]` 36 - 37 - The last three examples represent nested effects and are equivalent to functions of arity > 1 or functions that return functions. 38 - 39 - `And[A, B]` is the requirement for both `A` and `B` abilities. Notice in the last two examples, that their components are swapped. It is important to note that in `Fx.go`, _the *order* of the abilities on And requirements does not matter_ and they can be freely swapped/joined/unjoined. More on this when we talk about `And*` combinators. 40 - 41 - Also, note that `And[A, Nil]` is equivalent to just `A`. All of these types represent the same type of computation and an effect can be transformed to any of those types freely. 42 - 43 - ## `>1` arity functions as effects. 44 - 45 - Suppose you have a function that multiplies an string length by n. 46 - 47 - ```go 48 - func MulLen(s string, n int) int { 49 - return len(s) * n 50 - } 51 - ``` 52 - 53 - `MulLen` can be described by the following types: 54 - 55 - - `func(string, int) int` 56 - - `func(string) func(int) int` 57 - - `Fx[And[string, int], int]` 58 - - `Fx[string, Fx[int, int]]` 59 - - `Fx[int, Fx[string, int]]` 60 - - `Fx[And[int, string], int]` 61 - 62 - Note that `And[X, X]` is equivalent to just a single `X` requirement, and that `And[And[X, Y], And[Y, X]]` is also equivalent to `And[X, Y]`. 5 + See the subchapters for details on applying, shuffling, and providing requirements in Rust.
-48
book/src/requirements/apply.md
··· 1 - # Functional Requirements 2 - 3 - As we have seen in the previous [Context](context.html) chapter, you can ask for any type to be part of an effect's environment. 4 - 5 - You can also ask for functions of some type to be present and use them in your program descriptions without knowing their exact implementation. 6 - 7 - For example, suppose we ask for a function that takes an integer. And then apply it to a value. 8 - 9 - ```go 10 - // The type of the function from int to int 11 - type OnInt func(int) nit 12 - 13 - // Apply OnInt(12) 14 - var a Fx[OnInt, int] = Map(Ctx[OnInt](), func(f OnInt) int { 15 - return f(12) 16 - }) 17 - 18 - // fx.Apply is just an abbreviation of the previous code: 19 - // It takes the type of a unary function and the value to apply. 20 - var b Fx[OnInt, int] = Apply[OnInt](12) 21 - ``` 22 - 23 - Because `OnInt` is part of the environment, we only know its signature (its request and response type), but not the actual implementation of it. This way, different implementations of `OnInt` can be provided later. 24 - 25 - Now, suppose you have the following code: 26 - 27 - ```go 28 - type HttpRq string 29 - var url = HttpRq("https://example.org") 30 - 31 - // `Http` is the type of an *effect request* function. 32 - // it takes a plain value and creates an effect. 33 - type Http func(HttpRq) Fx[HttpService, HttpRs] 34 - 35 - // NOTE: applying to Http, creates a nested effect 36 - var a Fx[Http, Fx[HttpService, HttpRs]] = Apply[Http](url) 37 - 38 - // Flatenning creates an Anded environment. 39 - var b Fx[And[Http, HttpService], HttpRs] = AndJoin(a) 40 - 41 - 42 - // Same as previous two lines: 43 - var x Fx[And[Http, HttpService], HttpRs] = Suspend[Http](url) 44 - ``` 45 - 46 - `fx.Suspend` is an abbreviation of `AndJoin(Apply[Http](url))` for *effect requests* (functions returning an effect) 47 - 48 - This is the principle behind *suspended effect application* in `Fx.go` and is a fundamental part when we talk about effect [Requests](../requests.html) and [Handlers](../handlers.html).
-24
book/src/requirements/context.md
··· 1 - # Context Requirements 2 - 3 - The `Ctx[V]() Fx[V, V]` function creates an effect that requires some value `V` as part of the environment and evaluates to `V`. 4 - 5 - It can be used to request the presence of some services (interfaces or collections of methods that produce related effects) or more generally, an evidence that a value `V` is part of the environment. 6 - 7 - In the following example we simply map over an string to compute its length. 8 - 9 - ```go 10 - var eff Fx[string, int] = Map(Ctx[string](), LengthOfString) 11 - var result int = Eval(Provide(eff, "hello")) 12 - assert(result == len("hello")) 13 - ``` 14 - 15 - Calling the `Const[S](V) Fx[S, V]` is like maping a context over a constant function. 16 - As you might have guessed, `Pure` is defined in terms of `Const`. 17 - 18 - ```go 19 - var a Fx[string, int] = Map(Ctx[string](), func(_ string) int { return 42 }) 20 - var b Fx[string, int] = Const[string](42) 21 - 22 - var c Fx[Nil, int] = Pure(22) 23 - var d Fx[Nil, int] = Const[Nil](22) 24 - ```
-35
book/src/requirements/providers.md
··· 1 - # Providing Requirements 2 - 3 - *Effect requirements can be provided in any order* with no impact in the evaluation of the program. 4 - 5 - ## `Provide*` Combinators. 6 - 7 - These functions are used to eliminate requirements from effects. Only when `Nil` is the only remaining requirement, the effect can be evaluated. 8 - 9 - The result of provide functions will always be another effect, meaning that no computation has been performed yet, nor any side-effect produced. The result is just another effect but with less requirements. 10 - 11 - ```go 12 - // Discharges the single S requirement. 13 - func Provide(Fx[S, V], S) Fx[Nil, V] 14 - 15 - 16 - // Discharges the requirement of A by providing it. 17 - func ProvideLeft(Fx[And[A, B], V], A) Fx[B, V] 18 - 19 - // Discharges the requirement of B by providing it. 20 - func ProvideRight(Fx[And[A, B], V], B) Fx[A, V] 21 - 22 - // Discharges both A and B 23 - func ProvideBoth(Fx[And[A, B], V], A, B) Fx[Nil, V] 24 - 25 - 26 - 27 - // Provides A, the first part of the left And. 28 - func ProvideFirstLeft(Fx[And[And[A, C], And[B, D]], V], A) Fx[And[C, And[B, D]], V] 29 - 30 - // Provides B, the first part of the right And. 31 - func ProvideFirstRight(Fx[And[And[A, C], And[B, D]], V], B) Fx[And[And[A, C], D], V] 32 - 33 - // Provides A and B, the first part of both Ands. 34 - func ProvideFirsts(Fx[And[And[A, C], And[B, D]], V], A, B) Fx[And[C, D], V] 35 - ```
-33
book/src/requirements/shuffling.md
··· 1 - # Shuffling `And`ed requirements. 2 - 3 - An important thing to note is that in `Fx.go`, *requirements are identified by their type* and not by name or position as in regular functions. 4 - 5 - *Effect requirements can be freely re-arranged* with no impact in the meaning of the program. 6 - 7 - ## `And*` combinators. 8 - 9 - There are some functions (more will be added as they are found useful) that help you re-arrange `And`ed effect requirements. 10 - 11 - Shuffling And requirements is useful because you might want to provide some requirements first than others even if they are not in the head position of the requirement list. 12 - 13 - ```go 14 - // Since `And[A, A]` is equivalent to just `A`. 15 - // Used to collapse Nil requirements just before evaluation. 16 - func AndCollapse(Fx[And[A, A], V]) Fx[A, V] 17 - 18 - // Ands S with Nil in the effect requirements 19 - func AndNil(Fx[S, V]) Fx[And[S, Nil], V] 20 - 21 - // Swaps A and B. Note: this has no impact on how computation is 22 - // performed, since requirements can be freely re-arranged. 23 - func AndSwap(Fx[And[A, B], V]) Fx[And[B, A], V] 24 - 25 - 26 - // FlatMaps the inner effect into the outer by 27 - // Anding their requirements. 28 - func AndJoin(Fx[A, Fx[B, V]]) Fx[And[A, B], V] 29 - 30 - // Reverse of Join 31 - func AndDisjoin(Fx[And[A, B], V]) Fx[A, Fx[B, V]] 32 - 33 - ```
+19
book/src/resources.md
··· 1 + # Resource Management and Bracket Patterns 2 + 3 + fx-rs supports safe, composable resource management using bracket-like APIs and Rust’s ownership model. 4 + 5 + ## Example: File Handling 6 + 7 + ```rust 8 + use fx::Fx; 9 + 10 + fn with_file<'f>(path: &'f str) -> Fx<'f, (), String> { 11 + Fx::pure(move |_| format!("opened {}", path)) 12 + } 13 + 14 + let fx = with_file("foo.txt"); 15 + let result = fx.eval(); 16 + assert_eq!(result, "opened foo.txt"); 17 + ``` 18 + 19 + Resources are acquired, used, and released safely, with automatic cleanup and error handling.
+2 -9
book/src/rw.md
··· 1 1 # Reader/Writer 2 2 3 - A `Reader[T]` allows to read values of type `T` from the environment while `Writer[T]` allows to set them. 4 - 5 - - Implementation 3 + A `Reader<T>` allows reading values of type `T` from the environment, while `Writer<T>` allows setting them. 6 4 7 - - [read.go](https://github.com/vic/fx.go/blob/main/rw/read.go) 8 - - [write.go](https://github.com/vic/fx.go/blob/main/rw/write.go) 9 - 10 - - Tests [rw_test.go](https://github.com/vic/fx.go/blob/main/rw/rw_test.go) 11 - 12 - Read and Write Handlers take an effectful operation that can modify the external world. See `TestReadWrite`. 5 + Read and Write handlers take an effectful operation that can modify the external world. See the fx-rs codebase and tests for usage examples.
+102
book/src/state_lenses.md
··· 1 + # State Effects, Lenses, and Contexts in fx-rs 2 + 3 + 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. 4 + 5 + ## Modular State and Lenses 6 + 7 + 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. 8 + 9 + ### Example: Using a Lens 10 + 11 + ```rust 12 + use fx::{State, Lens}; 13 + 14 + // Type alias for state lens effect 15 + type LensEff = Fx<State, Value>; 16 + 17 + #[derive(Clone)] 18 + struct AppState { 19 + counter: u32, 20 + user: User, 21 + } 22 + 23 + #[derive(Clone)] 24 + struct User { 25 + name: String, 26 + } 27 + 28 + let lens = Lens::<AppState, u32>::new(); 29 + let get_counter = State::<AppState>::get().via(lens.zoom_out()); 30 + ``` 31 + 32 + ## Struct-Based Contexts and Pair 33 + 34 + fx-rs supports both tuple and struct-based contexts. The `Pair` trait allows you to use custom structs as effect environments, not just tuples. 35 + 36 + ### Example: Pair for Struct Context 37 + 38 + ```rust 39 + use fx::Pair; 40 + 41 + #[derive(Clone)] 42 + struct Ctx { a: i32, b: bool } 43 + 44 + impl Pair<i32, bool> for Ctx { 45 + fn fst(self) -> i32 { self.a } 46 + fn snd(self) -> bool { self.b } 47 + } 48 + ``` 49 + 50 + This enables ergonomic, named, and type-safe contexts for your effects. 51 + 52 + ## forall: Quantified Effects 53 + 54 + The `forall` combinator allows you to abstract over part of the context, making your effect generic over some state or ability. 55 + 56 + ### Example: forall 57 + 58 + ```rust 59 + use fx::{Fx, State}; 60 + 61 + let fx: Fx<'static, i32, i32> = State::<i32>::get(); 62 + let fx2 = fx.forall(|fx| fx.map(|n| n + 1)); 63 + let result = fx2.provide(41).eval(); 64 + assert_eq!(result, 42); 65 + ``` 66 + 67 + ## provide_part: Partial Context Provision 68 + 69 + `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. 70 + 71 + ### Example: provide_part 72 + 73 + ```rust 74 + use fx::Fx; 75 + 76 + let fx: Fx<'static, (i32, bool), i32> = Fx::value(7); 77 + let fx2 = fx.provide_part::<i32, bool>(1); // Now Fx<'static, bool, i32> 78 + ``` 79 + 80 + ## field_macro: Field-Based Lenses 81 + 82 + 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. 83 + 84 + ### Example: field_macro 85 + 86 + ```rust 87 + use field_macro::Field; 88 + 89 + #[derive(Field, Clone)] 90 + struct AppState { 91 + #[field] 92 + counter: u32, 93 + #[field] 94 + user: User, 95 + } 96 + 97 + // Now you can use AppState::counter() and AppState::user() as lenses. 98 + ``` 99 + 100 + ______________________________________________________________________ 101 + 102 + Lenses, struct-based contexts, and advanced combinators make fx-rs a powerful tool for modular, reusable, and testable stateful code.
+23
book/src/testing.md
··· 1 + # Testing Effectful Code 2 + 3 + fx-rs makes it easy to test effectful code by swapping in test handlers or mocks. 4 + 5 + ## Example: Mocking an Ability 6 + 7 + ```rust 8 + use fx::Fx; 9 + 10 + trait Http { fn get(&self, url: &str) -> String; } 11 + struct MockHttp; 12 + impl Http for MockHttp { fn get(&self, url: &str) -> String { format!("mocked: {}", url) } } 13 + 14 + fn fetch<'f, H: Http>(url: &'f str) -> Fx<'f, H, String> { 15 + Fx::pure(move |h: &H| h.get(url)) 16 + } 17 + 18 + let fx = fetch("/test"); 19 + let result = fx.run(&MockHttp); 20 + assert_eq!(result, "mocked: /test"); 21 + ``` 22 + 23 + You can also capture outputs, use property-based testing, and swap handlers for deterministic tests.
+31
book/src/uniqueness.md
··· 1 + # What Makes fx-rs Unique 2 + 3 + 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: 4 + 5 + ## 1. Programmable Handlers as First-Class Values 6 + 7 + 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. 8 + 9 + ## 2. Trait-Based Abilities and Contexts 10 + 11 + 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. 12 + 13 + ## 3. Composable Context Adaptation 14 + 15 + 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. 16 + 17 + ## 4. Direct-Style and Builder APIs 18 + 19 + 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. 20 + 21 + ## 5. Lenses and Fine-Grained State 22 + 23 + 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. 24 + 25 + ## 6. Structured Concurrency as Effects 26 + 27 + 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. 28 + 29 + ## 7. Macro-Based Ergonomics 30 + 31 + 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.