This example shows how to define your own capabilities and handlers for algebraic effects in Roux.
import com.cajunsystems.roux.data.Unit;
// Define your domain-specific capabilities as a sealed interface
public sealed interface LogCapability<R> extends Capability<R> {
record Info(String message) implements LogCapability<Unit> {}
record Debug(String message) implements LogCapability<Unit> {}
record Error(String message, Throwable error) implements LogCapability<Unit> {}
}
public sealed interface HttpCapability<R> extends Capability<R> {
record Get(String url) implements HttpCapability<String> {}
record Post(String url, String body) implements HttpCapability<String> {}
}// Production handler - performs real side effects
public class ProductionLogHandler {
private final Logger logger = Logger.getLogger("App");
private final CapabilityHandler<Capability<?>> handler = CapabilityHandler.builder()
.on(LogCapability.Info.class, info -> {
logger.info(info.message());
return Unit.unit();
})
.on(LogCapability.Debug.class, debug -> {
logger.fine(debug.message());
return Unit.unit();
})
.on(LogCapability.Error.class, error -> {
logger.severe(error.message());
return Unit.unit();
})
.build();
public CapabilityHandler<Capability<?>> handler() {
return handler;
}
}
// Test handler - captures logs for assertions
public class TestLogHandler {
private final List<String> logs = new ArrayList<>();
private final CapabilityHandler<Capability<?>> handler = CapabilityHandler.builder()
.on(LogCapability.Info.class, info -> {
logs.add("INFO: " + info.message());
return Unit.unit();
})
.on(LogCapability.Debug.class, debug -> {
logs.add("DEBUG: " + debug.message());
return Unit.unit();
})
.on(LogCapability.Error.class, error -> {
logs.add("ERROR: " + error.message());
return Unit.unit();
})
.build();
public CapabilityHandler<Capability<?>> handler() {
return handler;
}
public List<String> getLogs() {
return Collections.unmodifiableList(logs);
}
}// Define your effect using the generator API
Effect<Throwable, String> workflow = Effect.generate(ctx -> {
ctx.perform(new LogCapability.Info("Starting workflow"));
String data = ctx.perform(new HttpCapability.Get("https://api.example.com/data"));
ctx.perform(new LogCapability.Debug("Received: " + data));
return data;
}, handler);
// Run with production handler
EffectRuntime runtime = DefaultEffectRuntime.create();
String result = runtime.unsafeRun(workflow);@Test
void testWorkflow() throws Throwable {
TestLogHandler logHandler = new TestLogHandler();
TestHttpHandler httpHandler = new TestHttpHandler()
.withResponse("https://api.example.com/data", "{\"result\":\"success\"}");
// Compose handlers
CapabilityHandler<Capability<?>> composedHandler = CapabilityHandler.compose(
logHandler.handler(),
httpHandler.widen()
);
Effect<Throwable, String> workflow = Effect.generate(ctx -> {
ctx.perform(new LogCapability.Info("Starting"));
String data = ctx.perform(new HttpCapability.Get("https://api.example.com/data"));
ctx.perform(new LogCapability.Info("Done"));
return data;
}, composedHandler);
String result = runtime.unsafeRun(workflow);
assertEquals("{\"result\":\"success\"}", result);
assertEquals(2, logHandler.getLogs().size());
}- Separation of concerns: Effect description is separate from interpretation
- Testability: Swap handlers for testing without mocking
- Type safety: Sealed interfaces ensure exhaustive pattern matching
- Composability: Mix multiple capability types via
CapabilityHandler.compose() - Flexibility: Same effect, different interpretations (prod, test, tracing, etc.)
public class TracingHandler implements CapabilityHandler<Capability<?>> {
private final CapabilityHandler<Capability<?>> delegate;
private final List<Trace> traces = new ArrayList<>();
public record Trace(Capability<?> capability, Object result, long durationNanos) {}
@Override
public <R> R handle(Capability<?> capability) throws Exception {
long start = System.nanoTime();
R result = delegate.handle(capability);
long duration = System.nanoTime() - start;
traces.add(new Trace(capability, result, duration));
return result;
}
public List<Trace> getTraces() {
return Collections.unmodifiableList(traces);
}
}
// Wrap any handler with tracing
TracingHandler tracer = new TracingHandler(productionHandler);
Effect<Throwable, String> effect = Effect.generate(ctx -> {
// ... your code
}, tracer);
runtime.unsafeRun(effect);
tracer.getTraces().forEach(System.out::println);Convert capabilities to effects to use all Effect operators:
import static com.cajunsystems.roux.Effects.*;
// Convert capability to effect
Effect<Throwable, String> fetchEffect = new MyCapability.Fetch("https://api.com/data")
.toEffect()
.map(String::toUpperCase)
.catchAll(e -> Effect.succeed("default"));
// Run with handler
String result = runtime.unsafeRunWithHandler(fetchEffect, handler);
// Parallel execution
Effect<Throwable, Result> parallel = new MyCapability.Fetch("url1")
.toEffect()
.zipPar(new MyCapability.Fetch("url2").toEffect(), Result::new);
// Or with static helpers for 3+ effects
Effect<Throwable, Summary> summary = par(
new MyCapability.Fetch("url1").toEffect(),
new MyCapability.Fetch("url2").toEffect(),
new MyCapability.Fetch("url3").toEffect(),
Summary::new
);
// Use in generator context
Effect<Throwable, String> workflow = Effect.generate(ctx -> {
// Lift capability to effect for composition
Effect<Throwable, String> dataEffect = ctx.lift(new MyCapability.Fetch("url"))
.map(String::trim)
.map(String::toUpperCase);
String data = ctx.yield(dataEffect);
ctx.perform(new MyCapability.Log("Got: " + data));
return data;
}, handler);Benefits:
- All Effect operators work (map, flatMap, retry, timeout, zipPar, etc.)
- Handler is implicit - provided at runtime
- Clean, composable API
- Mix generator style with functional style