Skip to content

A effects library written in java leveraging modern features from JDK21+

License

Notifications You must be signed in to change notification settings

CajunSystems/roux

Repository files navigation

Roux

Roux Logo

A lightweight, pragmatic effect system for modern Java

Maven Central License: MIT


Roux is a foundational effect system for the JVM that embraces Java's native capabilities. Built on virtual threads and structured concurrency, Roux provides a clean, composable way to handle side effects while staying close to Java's natural behavior.

Why Roux?

  • 🧵 Virtual Thread Native - Built from the ground up for JDK 21+ virtual threads
  • 🎯 Pragmatic Design - No heavy abstractions; close to Java's natural behavior
  • 🔀 Composable Effects - Functional combinators for clean effect composition
  • ⚡ Cancellation Built-in - Interrupt-based cancellation at effect boundaries
  • 🎨 Type-Safe Errors - Explicit error channel: Effect<E, A>
  • 🔌 Pluggable Runtime - Swap execution strategies (virtual threads by default)
  • ⚙️ Fork/Fiber Support - Structured concurrency for parallel effect execution
  • 🎭 Algebraic Effects - Capability system for testable, composable side effects

Installation

Maven

<dependency>
    <groupId>com.cajunsystems</groupId>
    <artifactId>roux</artifactId>
    <version>0.1.0</version>
</dependency>

Gradle (Kotlin DSL)

implementation("com.cajunsystems:roux:0.1.0")

Gradle (Groovy)

implementation 'com.cajunsystems:roux:0.1.0'

Requirements: Java 21 or higher

Quick Example

import com.cajunsystems.roux.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

Effect<IOException, String> readFile = Effect.suspend(() -> 
    Files.readString(Path.of("config.txt"))
);

Effect<IOException, String> withFallback = readFile
    .catchAll(e -> Effect.succeed("default config"))
    .map(String::toUpperCase);

EffectRuntime runtime = DefaultEffectRuntime.create();
String result = runtime.unsafeRun(withFallback);

Core Concepts

Effects are Lazy

Effects are descriptions of computations, not computations themselves. They only execute when explicitly run.

Effect<Throwable, Integer> effect = Effect.succeed(42)
    .map(x -> x * 2);  // Not executed yet!

// Execution happens here
Integer result = runtime.unsafeRun(effect);

Composable Error Handling

Errors are first-class citizens in the type system.

Effect<IOException, String> readConfig = Effect.suspend(() -> 
    Files.readString(Path.of("config.json"))
);

Effect<IOException, Config> parseConfig = readConfig
    .map(json -> parseJson(json))
    .catchAll(e -> Effect.succeed(Config.DEFAULT));

Concurrent Effects with Fork/Fiber

Run effects concurrently and join their results.

Effect<Throwable, String> fetchUser = Effect.suspend(() -> 
    httpClient.get("/users/123")
);

Effect<Throwable, String> fetchOrders = Effect.suspend(() -> 
    httpClient.get("/orders?user=123")
);

Effect<Throwable, Dashboard> dashboard = fetchUser.fork()
    .flatMap(userFiber -> fetchOrders.fork()
        .flatMap(ordersFiber -> 
            userFiber.join().flatMap(user ->
                ordersFiber.join().map(orders ->
                    new Dashboard(user, orders)
                )
            )
        )
    );

Async Execution with Cancellation

Run effects asynchronously and cancel them when needed.

CancellationHandle handle = runtime.runAsync(
    longRunningEffect,
    result -> System.out.println("Done: " + result),
    error -> System.err.println("Failed: " + error)
);

// Cancel from another thread
handle.cancel();  // Effect stops at next boundary
handle.await();   // Wait for completion

Error Recovery and Transformation

Effect<IOException, Data> loadData = Effect.suspend(() -> 
    readFromDatabase()
);

Effect<AppError, Data> transformed = loadData
    .mapError(io -> new AppError("Database error: " + io.getMessage()))
    .orElse(Effect.suspend(() -> readFromCache()));

Generator-Style Effects with Capabilities

Write imperative-looking code that remains pure and testable using algebraic effects.

// Define your capabilities
sealed interface LogCapability<R> extends Capability<R> {
    record Info(String message) implements LogCapability<Void> {}
}

sealed interface HttpCapability<R> extends Capability<R> {
    record Get(String url) implements HttpCapability<String> {}
}

// Use them in generator-style effects
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.Info("Received: " + data));
    
    return data.toUpperCase();
}, handler);

// Swap handlers for testing - no mocking needed!
TestHandler testHandler = new TestHandler()
    .withHttpResponse("https://api.example.com/data", "test-data");

String result = runtime.unsafeRunWithHandler(workflow, testHandler);

// Or use capabilities as effects directly
Effect<Throwable, User> userEffect = new GetUser("123")
    .toEffect()  // Convert capability to effect
    .map(json -> parseJson(json, User.class))
    .retry(3)
    .timeout(Duration.ofSeconds(10));

User user = runtime.unsafeRunWithHandler(userEffect, handler);

Learn more: Capabilities Guide | Capability Recipes

Documentation

Key Features

Effect Combinators

  • map - Transform success values
  • flatMap - Chain effects sequentially
  • catchAll - Handle errors and recover
  • mapError - Transform error types
  • widen - Widen error type to Throwable (safe)
  • narrow - Narrow error type to specific exception (unsafe cast)
  • orElse - Fallback to alternative effect
  • attempt - Convert to Either<E, A> for explicit handling
  • zipPar - Run effects in parallel and combine results

Structured Concurrency

  • Effect.scoped(body) - Create a scope for structured concurrency
  • scope.fork(effect) - Fork effect within scope with automatic cleanup
  • effect.forkIn(scope) - Convenience method to fork in a scope
  • Automatic cancellation on scope exit (success, error, or early return)
  • Built on Java's StructuredTaskScope (JEP 453)

Concurrency

  • fork() - Run effect on a separate virtual thread, returns Fiber<E, A>
  • join() - Wait for forked effect to complete
  • interrupt() - Cancel a running fiber
  • zipPar(other, combiner) - Parallel execution with result combination
  • Effects.par() - Static helpers for 2, 3, 4 parallel effects
  • Automatic cancellation at effect boundaries

Runtime Execution

  • unsafeRun(effect) - Synchronous execution, throws on error
  • unsafeRunWithHandler(effect, handler) - Run with capability handler
  • runAsync(effect, onSuccess, onError) - Asynchronous execution with callbacks
  • CancellationHandle - Control async execution (cancel, await)

Getting Started

Requirements

  • JDK 21 or higher (for virtual threads)
  • Gradle or Maven

Installation

Gradle:

dependencies {
    implementation 'com.cajunsystems:roux:0.1.0'
}

Maven:

<dependency>
    <groupId>com.cajunsystems</groupId>
    <artifactId>roux</artifactId>
    <version>0.1.0</version>
</dependency>

Examples

Simple HTTP Client with Retry

Effect<IOException, String> fetchWithRetry(String url) {
    return Effect.suspend(() -> httpClient.get(url))
        .catchAll(e -> Effect.suspend(() -> {
            Thread.sleep(1000);
            return httpClient.get(url);
        }));
}

Parallel Data Fetching

import static com.cajunsystems.roux.Effects.*;

// Verbose way
Effect<Throwable, Summary> fetchSummary() {
    return users.fork().flatMap(usersF ->
           orders.fork().flatMap(ordersF ->
               usersF.join().flatMap(u ->
                   ordersF.join().map(o ->
                       new Summary(u, o)
                   )
               )
           )
       );
}

// Clean way with zipPar
Effect<Throwable, Summary> fetchSummary() {
    return users.zipPar(orders, Summary::new);
}

// Or with static helper for 3+ effects
Effect<Throwable, Dashboard> fetchDashboard() {
    return par(users, orders, preferences, Dashboard::new);
}

Background Task with Timeout

CancellationHandle handle = runtime.runAsync(
    longTask,
    result -> System.out.println("Completed: " + result),
    error -> System.err.println("Failed: " + error)
);

// Timeout after 5 seconds
if (!handle.await(Duration.ofSeconds(5))) {
    handle.cancel();
    System.out.println("Task timed out");
}

Known Limitations

Deep Effect Chains

Roux uses recursive execution for effect composition. Very deep effect chains (>1000 nested flatMap operations) may cause stack overflow.

Workaround: Use loops inside suspend() rather than recursive flatMap chains:

// ❌ Avoid: Deep recursive chains
Effect<Throwable, Integer> effect = Effect.succeed(0);
for (int i = 0; i < 100000; i++) {
    effect = effect.flatMap(n -> Effect.succeed(n + 1));
}

// ✅ Prefer: Loop inside suspend
Effect<Throwable, Integer> effect = Effect.suspend(() -> {
    int result = 0;
    for (int i = 0; i < 100000; i++) {
        result = result + 1;
    }
    return result;
});

Stack Safety: Roux uses trampolined execution by default, providing true stack safety for arbitrarily deep effect chains. You can chain millions of flatMap operations without stack overflow.

Roadmap

  • Core effect system with error channel
  • Basic combinators (map, flatMap, catchAll)
  • Boundary-based cancellation
  • Async execution with CancellationHandle
  • Fork/Fiber for concurrent effects
  • Algebraic effects via capabilities
  • Generator-style effect building
  • Scoped structured concurrency
  • Stack-safe trampolined execution
  • Retry policies with backoff
  • Resource management (bracket, ensuring)
  • Race and timeout combinators
  • Environment/Layer system for dependency injection

Related Projects

  • Cajun - Actor framework built on Roux and modern Java 21+

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - see LICENSE file for details


Built with ❤️ for the Java community

About

A effects library written in java leveraging modern features from JDK21+

Resources

License

Stars

Watchers

Forks

Languages