Eff Lifting¶
Builders for lifting operations into the LanguageExt Eff monad — the core pattern for effect-based source generators.
See also: Overview | Types | Expressions | Extensions
Overview¶
LanguageExt's Eff<RT, A> monad captures side effects as data. Wrapping an operation into Eff is called lifting. There are two lifting APIs:
| Builder | Pattern | When to use |
|---|---|---|
EffLift |
liftEff<RT, A>(rt => ...) |
General-purpose — result type specified per call |
EffLiftIO |
Eff<RT, A>.LiftIO(rt => ...) |
Terminal operations — Eff type already known |
Both are created through the EffExpression entry point:
using Deepstaging.Roslyn.LanguageExt.Expressions;
using Deepstaging.Roslyn.LanguageExt;
var lift = EffExpression.Lift("RT", "rt");
var liftIO = EffExpression.LiftIO(LanguageExtTypes.Eff("RT", "int"), "rt");
EffLift¶
Builds liftEff<RT, A>(...) expressions. The result type is specified per method call.
Methods¶
| Method | Signature | Produces |
|---|---|---|
Async |
Async(result, expr) |
liftEff<RT, A>(async rt => await expr) |
AsyncVoid |
AsyncVoid(expr) |
liftEff<RT, Unit>(async rt => { await expr; return unit; }) |
AsyncOptional |
AsyncOptional(optionType, expr) |
liftEff<RT, Option<T>>(async rt => Optional(await expr)) |
AsyncNonNull |
AsyncNonNull(result, expr) |
liftEff<RT, A>(async rt => (await expr)!) |
Sync |
Sync(result, expr) |
liftEff<RT, A>(rt => expr) |
SyncVoid |
SyncVoid(expr) |
liftEff<RT, Unit>(rt => { expr; return unit; }) |
SyncOptional |
SyncOptional(optionType, expr) |
liftEff<RT, Option<T>>(rt => Optional(expr)) |
SyncNonNull |
SyncNonNull(result, expr) |
liftEff<RT, A>(rt => (expr)!) |
Body |
Body(result, lambdaBody) |
liftEff<RT, A>(lambdaBody) — escape hatch |
Examples¶
var lift = EffExpression.Lift("RT", "rt");
// Async value
lift.Async("int", "rt.Db.Users.CountAsync()");
// → liftEff<RT, int>(async rt => await rt.Db.Users.CountAsync())
// Async void (returns Unit)
lift.AsyncVoid("rt.Db.SaveChangesAsync()");
// → liftEff<RT, Unit>(async rt => { await rt.Db.SaveChangesAsync(); return unit; })
// Async optional (nullable → Option)
lift.AsyncOptional(LanguageExtTypes.Option("User"), "rt.Db.Users.FindAsync(id)");
// → liftEff<RT, global::LanguageExt.Option<User>>(async rt => Optional(await rt.Db.Users.FindAsync(id)))
// Async non-null (null-forgiving assertion)
lift.AsyncNonNull("User", "rt.Db.Users.FindAsync(id)");
// → liftEff<RT, User>(async rt => (await rt.Db.Users.FindAsync(id))!)
// Sync value
lift.Sync("string", "rt.Config.ConnectionString");
// → liftEff<RT, string>(rt => rt.Config.ConnectionString)
// Custom body (escape hatch)
lift.Body("int", "rt => { var x = rt.Get(); return x + 1; }");
// → liftEff<RT, int>(rt => { var x = rt.Get(); return x + 1; })
AsyncOptional requires OptionTypeRef
AsyncOptional and SyncOptional accept OptionTypeRef, not string. This prevents mistakes like passing a raw type name — you must go through LanguageExtTypes.Option() which ensures the Option<T> wrapping is correct.
EffLiftIO¶
Builds Eff<RT, A>.LiftIO(...) expressions. The Eff type is captured at construction — individual methods don't need the result type.
Methods¶
| Method | Signature | Produces |
|---|---|---|
Async |
Async(expr) |
Eff<RT, A>.LiftIO(async rt => await expr) |
AsyncVoid |
AsyncVoid(expr) |
Eff<RT, A>.LiftIO(async rt => { await expr; return unit; }) |
AsyncOptional |
AsyncOptional(expr) |
Eff<RT, A>.LiftIO(async rt => Optional(await expr)) |
AsyncNonNull |
AsyncNonNull(expr) |
Eff<RT, A>.LiftIO(async rt => (await expr)!) |
Sync |
Sync(expr) |
Eff<RT, A>.LiftIO(rt => expr) |
SyncVoid |
SyncVoid(expr) |
Eff<RT, A>.LiftIO(rt => { expr; return unit; }) |
SyncOptional |
SyncOptional(expr) |
Eff<RT, A>.LiftIO(rt => Optional(expr)) |
SyncNonNull |
SyncNonNull(expr) |
Eff<RT, A>.LiftIO(rt => (expr)!) |
Body |
Body(lambdaBody) |
Eff<RT, A>.LiftIO(lambdaBody) — escape hatch |
Examples¶
var effType = LanguageExtTypes.Eff("RT", "int");
var io = EffExpression.LiftIO(effType, "rt");
io.Async("query(rt).CountAsync(token)");
// → global::LanguageExt.Eff<RT, int>.LiftIO(async rt => await query(rt).CountAsync(token))
io.SyncVoid("rt.Cache.Clear()");
// → global::LanguageExt.Eff<RT, int>.LiftIO(rt => { rt.Cache.Clear(); return unit; })
LiftingStrategy¶
An enum that describes how a method call should be lifted, paired with a dispatch method that routes to the correct EffLift call.
Variants¶
| Strategy | Async/Sync | Return shape | Lambda pattern |
|---|---|---|---|
AsyncValue |
Async | Value | async rt => await expr |
AsyncVoid |
Async | Unit | async rt => { await expr; return unit; } |
AsyncOptional |
Async | Option | async rt => Optional(await expr) |
AsyncNonNull |
Async | Non-null | async rt => (await expr)! |
SyncValue |
Sync | Value | rt => expr |
SyncVoid |
Sync | Unit | rt => { expr; return unit; } |
SyncOptional |
Sync | Option | rt => Optional(expr) |
SyncNonNull |
Sync | Non-null | rt => (expr)! |
Dispatch¶
EffLift.Lift() is the dispatch method — call it with a strategy, result type, and expression:
var lift = EffExpression.Lift("RT", "rt");
// Dispatch based on a strategy determined at analysis time
LiftingStrategy strategy = LiftingStrategy.AsyncOptional;
string expr = lift.Lift(strategy, "User", "rt.Service.FindAsync(id)");
// → liftEff<RT, global::LanguageExt.Option<User>>(async rt => Optional(await rt.Service.FindAsync(id)))
When to use LiftingStrategy
Use LiftingStrategy when the lifting pattern is determined dynamically — for example, by analyzing a method's return type and async nature. This eliminates switch statements in your generator code.
EffReturnType¶
Computes the Eff return type for a given strategy — essential for building method signatures:
LiftingStrategy.AsyncValue.EffReturnType("int");
// → "int" (passthrough)
LiftingStrategy.AsyncOptional.EffReturnType("User");
// → global::LanguageExt.Option<User>
LiftingStrategy.AsyncVoid.EffReturnType("ignored");
// → global::LanguageExt.Unit
| Strategy category | EffReturnType behavior |
|---|---|
*Value, *NonNull |
Returns resultType as-is |
*Optional |
Wraps in Option<T> via LanguageExtTypes.Option() |
*Void |
Returns Unit via LanguageExtTypes.Unit |
LiftingStrategyAnalysis¶
Extension methods on ValidSymbol<IMethodSymbol> that automatically determine the lifting strategy and result type from a Roslyn method symbol. This eliminates manual analysis logic in your generator.
DetermineLiftingStrategy¶
Analyzes a method's async nature and return type nullability to pick the correct LiftingStrategy:
// Given a ValidSymbol<IMethodSymbol> from Roslyn analysis:
LiftingStrategy strategy = method.DetermineLiftingStrategy();
| Method signature | Determined strategy |
|---|---|
Task SendAsync(string to) |
AsyncVoid |
Task<int> CountAsync() |
AsyncValue |
Task<User?> FindAsync(int id) |
AsyncOptional |
void Clear() |
SyncVoid |
int Count() |
SyncValue |
User? Find(int id) |
SyncOptional |
The analysis inspects AsyncMethodKind, InnerTaskType, nullable annotations, and ReturnsVoid — all via the ValidSymbol projection from Deepstaging.Roslyn.
EffectResultType¶
Computes the raw (unwrapped) result type for a method given its strategy:
LiftingStrategy strategy = method.DetermineLiftingStrategy();
string resultType = method.EffectResultType(strategy);
| Strategy category | Result |
|---|---|
*Void |
"Unit" |
AsyncValue, AsyncOptional, AsyncNonNull |
Inner type of Task<T> / ValueTask<T> |
SyncValue, SyncOptional, SyncNonNull |
The method's return type directly |
The returned type is always the unwrapped inner type (e.g., "User" not "Option<User>"). Wrapping is handled downstream by EffReturnType and Lift.
DetermineLiftingStrategy + EffectResultType + Lift = zero manual analysis
These three methods form a complete pipeline: analyze the method, extract its result type, then generate the lift expression — no switch statements needed in your generator code.
Putting It Together¶
A complete generator pattern that uses LiftingStrategyAnalysis to fully automate effect method generation from Roslyn symbols:
using Deepstaging.Roslyn.LanguageExt.Expressions;
using Deepstaging.Roslyn.LanguageExt.Extensions;
using Deepstaging.Roslyn.LanguageExt;
var lift = EffExpression.Lift("RT", "rt");
// For each method on the source interface:
foreach (var method in sourceType.QueryMethods())
{
// 1. Determine how the method should be lifted
var strategy = method.DetermineLiftingStrategy();
var resultType = method.EffectResultType(strategy);
// 2. Build the call expression
var callExpr = $"rt.Service.{method.Name}({paramList})";
// 3. Generate the effect method — strategy drives everything
type = type.AddMethod(method.Name, m => m
.AsEffMethod("RT", "IHasService", strategy.EffReturnType(resultType))
.WithExpressionBody(lift.Lift(strategy, resultType, callExpr)));
}
This generates correct code regardless of whether the source method is async/sync, returns void, returns nullable, or returns a value — the strategy handles all the variation.