Skip to content

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 from runs sequentially — errors abort the pipeline
  • RT constraints 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

from _ in Guard<AppRuntime>(order.Status == Pending,
             Error.New(409, "Order already processed"))

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:

from _ in Guard<RT>(order.Status == OrderStatus.Pending,
    Error.New(409, "Order already processed"))

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:

from result in existingOrder.MatchEff<RT, Order, Unit>(
    Some: order => AppStore.Orders.Update<RT>(order with { Status = Updated }),
    None: () => AppStore.Orders.Create<RT>(newOrder))