Composition¶
Effects compose using LINQ-style from/select syntax. Each binding sequences an effect — if any step fails, the pipeline short-circuits.
The Eff<RT, A> Type¶
Eff<RT, A> is a lazy computation that requires environment RT, produces A, and may fail with Error. Nothing executes until you call RunAsync against a concrete runtime.
from/select Pipelines¶
public static Eff<RT, Unit> SendWelcomeEmail<RT>(string userId)
where RT : IHasEmailService, IHasUserService =>
from user in UserEffects.UserService.GetByIdAsync<RT>(userId)
from _ in EmailEffects.EmailService.ValidateAsync<RT>(user.Email)
>> EmailEffects.EmailService.SendAsync<RT>(
user.Email, "Welcome!", $"Hello {user.Name}")
select unit;
- Each
fromruns sequentially — errors abort the pipeline RTconstraints accumulate across all effects used>>chains fire-and-forget steps without discard variables
Key Operators¶
| Pattern | Syntax | Use When |
|---|---|---|
| Sequence, keep value | from x in effect |
You need x later |
| Sequence, discard | effect1 >> effect2 |
Fire-and-forget |
| Transform result | .Map(x => ...) |
Sync transformation |
| Chain to next effect | .Bind(x => nextEffect) |
Next step depends on result |
| Unwrap Option | .Require(error) |
Convert Option<T> → T or fail |
| Assert condition | Guard<RT>(cond, error) |
Precondition check |
| Validate multiple | Validate<RT>(...) |
Accumulate all failures |
| Map over collection | items.TraverseEff(f) |
Apply effect to each item |
| Batch effects | effects.SequenceEff() |
Run all, collect results |
| Side effects | items.ForEachEff(f) |
Apply and discard |
| Transform error | .MapFail(e => ...) |
Add domain context |
| Recover | eff \| @catch(e => ...) |
Fallback on failure |
| Debug | .Tap(x => ...) |
Log without changing value |
| Lift value | SuccessEff<RT, T>(val) |
Wrap a constant |
| Lift failure | FailEff<RT, T>(error) |
Explicit failure |
| Lift side effect | liftEff<RT, T>(rt => ...) |
Access runtime directly |
Deepstaging Extensions¶
Require — Unwrap Option or Fail¶
from user in UserStore.Users.FindById<AppRuntime>(userId)
.Require(Error.New(404, "User not found"))
// user is T, not Option<T>
Guard — Assert a Condition¶
Validate — Accumulate Multiple Checks¶
from _ in Validate<AppRuntime>(
(cmd.Quantity > 0, Error.New("Quantity must be positive")),
(cmd.Price >= 0, Error.New("Price cannot be negative")))
TraverseEff — Map Collection Through Effects¶
from items in itemIds.TraverseEff(id =>
CatalogStore.Items.Get<AppRuntime>(id)
.Require(Error.New(404, $"Item {id} not found")))
// items is Seq<CatalogItem>
ForEachEff — Apply and Discard¶
from _ in expiredOrders.ForEachEff(order =>
OrderStore.Orders.Save<AppRuntime>(order with { Status = Cancelled }) >>
NotifyEffects.Send<AppRuntime>(order.CustomerId, "Order expired"))
Lifting Strategy¶
The generator selects the lifting strategy by return type:
| Return Type | Lifted As | Effect Result |
|---|---|---|
Task<T> / ValueTask<T> |
liftEff<RT, T>(async rt => await ...) |
T |
Task / ValueTask |
liftEff<RT, Unit>(async rt => { ...; return unit; }) |
Unit |
Task<T?> / ValueTask<T?> |
liftEff<RT, Option<T>>(async rt => Optional(...)) |
Option<T> |
T (sync) |
liftEff<RT, T>(rt => ...) |
T |
void |
liftEff<RT, Unit>(...) |
Unit |
Nullable returns become Option<T>. CancellationToken parameters get default automatically.
Runtime Effect Utilities¶
The runtime provides additional effect utilities beyond the generated module methods. These fill common patterns for handler implementation:
Reference¶
| Method | Purpose | When to use |
|---|---|---|
Require<RT, T>(Eff<RT, Option<T>>, Error) |
Unwrap Option or fail | "Get entity or 404" |
Guard<RT>(bool, Error) |
Assert condition or fail | Precondition checks |
Validate<RT>(params (bool, Error)[]) |
Applicative — collect ALL errors | Form validation |
TraverseEff<RT, A, B>(IEnumerable<A>, Func) |
Sequential effectful map | Process items with effects |
SequenceEff<RT, A>(IEnumerable<Eff<RT, A>>) |
Sequence effects | Run list of effects in order |
ForEachEff<RT, A>(IEnumerable<A>, Func) |
Effectful foreach | Side-effects over collection |
MatchEff<RT, T, R>(Option<T>, Some, None) |
Pattern-match Option → effects | Branching on optional values |
Mutate<RT>(Action) |
Lift void to Eff | Side-effects (logging, metrics) |
Tap<RT, A>(Eff<RT, A>, Action<A>) |
Side-effect without changing value | Logging in pipelines |
unitEff<RT>() |
SuccessEff<RT, Unit>(unit) shorthand |
Terminal "return nothing" |
AsUnit<RT, A>(Eff<RT, A>) |
Discard value → Unit | Ignore return values |
Examples¶
Require — unwrap or fail:
from order in AppStore.Orders.GetById<RT>(orderId)
.Require(Error.New(404, $"Order {orderId} not found"))
Guard — precondition:
Validate — applicative (collects ALL errors):
from validated in Validate<RT>(
(cmd.Name.Length > 0, Error.New("name", "required")),
(cmd.Amount > 0, Error.New("amount", "must_be_positive")))
TraverseEff — sequential map with effects:
from results in lineItems.TraverseEff(item =>
InventoryEffects.Reserve<RT>(item.ProductId, item.Quantity))
MatchEff — branch on Option: