Skip to content

Releases: CajunSystems/roux

v0.3.0

18 Apr 14:34
e48b751

Choose a tag to compare

Roux 0.3.0 — Type-Safe Capability Environments & Virtual-Clock Testing

Roux 0.3.0 introduces compile-time capability tracking via phantom types, a ZIO-style layer system for wiring environments, and a virtual-clock TestRuntime for instant, deterministic sleep tests. The Effect API is unchanged for the common case — these features layer on top as opt-in additions.

✨ Highlights

✅ Compile-time capability tracking with EffectWithEnv

EffectWithEnv<R,E,A> wraps any Effect<E,A> and tracks, at compile time, which capabilities it needs via the phantom type R. The compiler rejects run() calls that don't supply the right environment — no missing-handler surprises at runtime.

// Define a capability
sealed interface StoreOps extends Capability<String> {
    record Get(String key) implements StoreOps {}
    record Put(String key, String value) implements StoreOps {}
}

// Build a typed environment
Map<String, String> store = new HashMap<>();
HandlerEnv<StoreOps> env = HandlerEnv.of(StoreOps.class, cap -> switch (cap) {
    case StoreOps.Get  g -> store.getOrDefault(g.key(), "missing");
    case StoreOps.Put  p -> { store.put(p.key(), p.value()); yield "ok"; }
});

// Wrap an effect — R is inferred as StoreOps
EffectWithEnv<StoreOps, RuntimeException, String> typedEffect =
        EffectWithEnv.of(Effect.from(new StoreOps.Get("greeting")));

// Run — compiler verifies env covers StoreOps
String result = typedEffect.run(env, DefaultEffectRuntime.create());

✅ Layer system for capability wiring

Layer<RIn,E,ROut> is a functional interface that builds a HandlerEnv<ROut> from a HandlerEnv<RIn>. Layers compose horizontally (and) and vertically (andProvide), producing a single layer that satisfies multiple requirements.

// Leaf layer — no input requirements
Layer<Empty, RuntimeException, StoreOps> storeLayer =
        Layer.succeed(StoreOps.class, cap -> switch (cap) { ... });

Layer<Empty, RuntimeException, LogOps> logLayer =
        Layer.succeed(LogOps.class, cap -> switch (cap) { ... });

// Horizontal composition — satisfies both StoreOps and LogOps
Layer<Empty, Throwable, With<StoreOps, LogOps>> combined = storeLayer.and(logLayer);

// Build the environment and run
HandlerEnv<With<StoreOps, LogOps>> env =
        combined.build(HandlerEnv.empty()).unsafeRun(runtime);

✅ Phantom types: Empty and With<A,B>

Empty marks effects that need no capabilities. With<A,B> represents a capability environment union — both are compile-time-only interfaces that are never instantiated.

// Effect that needs nothing
EffectWithEnv<Empty, RuntimeException, Integer> pure =
        EffectWithEnv.pure(Effect.succeed(42));
pure.run(HandlerEnv.empty(), runtime); // compiles

// Effect that needs both StoreOps and LogOps
EffectWithEnv<With<StoreOps, LogOps>, IOException, Unit> combined = ...;
combined.run(storeEnv.and(logEnv), runtime); // compiles
// combined.run(storeEnv, runtime);           // compile error — missing LogOps

✅ First-class Effect.Sleep and TestRuntime

Effect.sleep(Duration) now creates an Effect.Sleep node instead of an opaque Suspend node. This makes sleep interceptable by custom runtimes. TestRuntime uses this to advance a TestClock instead of blocking — sleep-dependent effects run instantly in tests.

TestRuntime runtime = TestRuntime.create();

// Runs instantly — no real sleep
runtime.unsafeRun(Effect.sleep(Duration.ofSeconds(30)));

// Virtual clock advanced by 30 seconds
assertEquals(Duration.ofSeconds(30), runtime.clock().currentTime());

// Chain multiple sleeps
runtime.unsafeRun(
    Effect.sleep(Duration.ofSeconds(5))
          .flatMap(__ -> Effect.sleep(Duration.ofSeconds(5)))
);
assertEquals(Duration.ofSeconds(40), runtime.clock().currentTime());

CapabilityHandler.forType() — new recommended API

CapabilityHandler.forType(Class<F>) anchors the builder to a capability family, giving the compiler enough information to infer lambda parameter types without explicit casts. The old builder() is deprecated and will be removed in a future release.

// Old (deprecated)
var handler = CapabilityHandler.builder()
    .on(StoreOps.Get.class,  (StoreOps.Get g) -> store.get(g.key()))
    .build();

// New (recommended)
var handler = CapabilityHandler.forType(StoreOps.class)
    .on(StoreOps.Get.class,  g -> store.get(g.key()))  // type inferred
    .build();

parTraverse — parallel map over a collection

Effects.parTraverse collapses the two-step "build list then parAll" pattern into one declarative call. parTraverseEither is the tolerant variant — it collects both successes and failures instead of short-circuiting.

// Before
List<Effect<Throwable, Result>> effs = new ArrayList<>();
for (Item item : items) {
    effs.add(checkInventory(item).<Throwable>toEffect());
}
Effect<Throwable, List<Result>> result = parAll(effs);

// After
Effect<Throwable, List<Result>> result =
    Effects.parTraverse(items, item -> checkInventory(item).<Throwable>toEffect());

// Collect all results — no short-circuit on failure
Effect<Throwable, List<Either<Throwable, Result>>> mixed =
    Effects.parTraverseEither(items, item -> checkInventory(item).<Throwable>toEffect());

Schedule<A, B> — composable repeat-on-success scheduling

RetryPolicy handles the failure path (retry on error). Schedule handles the success path — repeat an effect on a cadence, while a predicate holds, or for a fixed number of iterations, and optionally accumulate the outputs. The two compose naturally.

// Poll every 2 s, up to 10 times, until done — collect all status values
Schedule<Status, List<Status>> schedule = Schedule
    .<Status>fixed(Duration.ofSeconds(2))
    .recurs(10)
    .whileOutput(s -> !s.isDone())
    .collect();

Effect<Throwable, List<Status>> polling = schedule.repeat(checkStatus);

// Retry transient failures, then repeat on success
schedule.repeat(
    unstableCheck.retry(RetryPolicy.exponential(Duration.ofMillis(50)).maxAttempts(3))
);

API surface:

  • Factories: fixed(Duration), exponential(Duration), immediate()
  • Termination: recurs(n), whileOutput(pred), untilOutput(pred), maxDelay(Duration), jittered(factor)
  • Accumulation: collect() — folds all outputs into List<A>
  • Execution: repeat(effect) — stack-safe, integrates with the trampolined runtime

Effect.effect() — no-handler generator entry point

When writing generator-style blocks that only use ctx.yield() and ctx.call() (no capability dispatch), the CapabilityHandler argument to Effect.generate() was dead ceremony. Effect.effect() removes it.

// Before
Effect<IOException, String> pipeline = Effect.generate(ctx -> {
    String a = ctx.yield(fetchA());
    String b = ctx.yield(fetchB());
    return a + b;
}, CapabilityHandler.builder().build()); // dead noise

// After
Effect<IOException, String> pipeline = Effect.effect(ctx -> {
    String a = ctx.yield(fetchA());
    String b = ctx.yield(fetchB());
    return a + b;
});

⚠️ Breaking Changes

Effect.Sleep is a new sealed subtype (source-breaking)

Sleep<E> is a new permitted type in the public sealed interface Effect. Any exhaustive switch expression or statement over all Effect variants will fail to compile without a Sleep branch.

Before (compiles in 0.2.x, fails in 0.3.0):

return switch (effect) {
    case Effect.Pure<E,A>    p -> ...
    case Effect.Fail<E,A>    f -> ...
    case Effect.Suspend<E,A> s -> ...
    case Effect.Sleep<E>     s -> ...  // ← MISSING — compile error in 0.3.0
    // ... all other cases
};

After (required in 0.3.0):

case Effect.Sleep<E> s -> performSleep(s.duration()); yield result;

Custom EffectRuntime implementations must add a Sleep branch and call their sleep implementation there.

CapabilityHandler.Builder is now generic (source-breaking)

The nested class changed from Builder to Builder<F extends Capability<?>>. Explicit raw-type references produce compiler warnings.

Before (raw type, 0.2.x):

CapabilityHandler.Builder builder = CapabilityHandler.builder();

After (typed, 0.3.0):

CapabilityHandler.Builder<MyCapability> builder =
        CapabilityHandler.forType(MyCapability.class);

🔄 Migration Guide

Upgrade from 0.2.x to 0.3.0 with the following checklist:

  • Add Sleep branch to exhaustive Effect switches — add case Effect.Sleep<?> s -> ... to any switch expression or statement that covers all Effect variants
  • Update custom EffectRuntime implementations — intercept Effect.Sleep and call your sleep implementation (e.g. Thread.sleep(sleep.duration()))
  • Replace raw CapabilityHandler.Builder references — use CapabilityHandler.Builder<?> or switch to CapabilityHandler.forType(MyCapability.class)
  • Replace CapabilityHandler.builder() calls — use CapabilityHandler.forType(MyCapability.class) instead; behaviour is identical, type inference is better
  • No changes needed for Effect<E,A> code — all existing map, flatMap, catchAll, fork, retry, timeout, and other combinators are unchanged

⚠️ Known Limitations

CapabilityHandler.Builder.build() — flat sealed hierarchies only

The built handler resolves capabilities by checking the concrete class and then its direct interfaces. It correctly handles the standard pattern where sealed subtypes directly implement the registered interface. However, nested sealed hierarchies — where a concrete capability implements a supertype that is itself a subtype of the registered interface — are not resolved and will throw UnsupportedOperationException at runtime.


🧪 Testing

  • ~93 new tests added across Milestones 1 and 2 (effect laws, test utilities, typed effects)
  • Effect law tests — 11 algebraic laws v...
Read more

v0.2.2

05 Mar 11:46
9331d62

Choose a tag to compare

Roux v0.2.2

Patch release with targeted runtime fixups and documentation consistency improvements.

Fixed

  • Resource.flatMap(...) now correctly composes finalizers without relying on a placeholder f.apply(null) path.
  • Outer resource release is now guaranteed when inner resource acquisition fails during flatMap composition.

Documentation

  • Capability docs and examples now consistently use Unit for side-effect-only capabilities instead of Void.
  • Replaced yield (R) null patterns in examples with Unit.unit().

Roux 0.2.1

03 Mar 04:53
5f0f08e

Choose a tag to compare

Roux 0.2.1 - Scoped Fork Context Fixes and Better Diagnostics

Roux 0.2.1 is a bugfix release focused on structured-concurrency correctness and clearer capability error reporting.

✨ Highlights

✅ Scoped fork now preserves capability context

  • scope.fork(...) now inherits the current ExecutionContext
  • Forked effects now see the same installed CapabilityHandler as the parent flow
  • Behavior is now consistent with effect.fork()

✅ Missing capability errors are now actionable

  • Missing handler failures now include the exact capability type that was attempted
  • Added a dedicated MissingCapabilityHandlerException for cleaner typed handling
  • Error guidance points users to unsafeRunWithHandler(...) or embedded generator handlers

✅ Cleaner failure propagation from fibers

  • Fiber.join() no longer double-wraps runtime exceptions
  • Stack traces are easier to read and root causes are easier to identify

🧪 Testing

  • Added scoped-fork regression tests for:
    • capability-handler inheritance inside scoped forks
    • missing-handler diagnostics including capability type
  • Existing scope-related tests continue to pass

📦 Installation

Maven

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

Gradle (Kotlin DSL)

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

Gradle (Groovy)

implementation 'com.cajunsystems:roux:0.2.1'

🔗 Links


Full Changelog: v0.2.0...v0.2.1

Roux 0.2.0

02 Mar 15:31

Choose a tag to compare

Roux 0.2.0 - Reliability, Resource Safety, and New Core Combinators

We're excited to announce Roux 0.2.0 - a major update focused on runtime reliability, better resource safety, richer effect combinators, and improved ergonomics across the API.

✨ Highlights

✅ New Effect Constructors

  • Effect.unit()
  • Effect.runnable(Runnable)
  • Effect.sleep(Duration)
  • Effect.when(boolean, Effect)
  • Effect.unless(boolean, Effect)

✅ New Combinators

  • tap(Consumer<A>)
  • tapError(Consumer<E>)
  • retry(int)
  • retryWithDelay(int, Duration)
  • retry(RetryPolicy)
  • timeout(Duration)

✅ New Concurrency Helpers (Effects)

  • Effects.race(List) / Effects.race(ea, eb)
  • Effects.sequence(List)
  • Effects.traverse(List, Function)
  • Effects.parAll(List)

✅ Resource Management

  • New Resource<A> type with:
    • Resource.make(acquire, release)
    • Resource.fromCloseable(acquire)
    • resource.use(f)
    • Resource.ensuring(effect, finalizer)

✅ Retry Policies

  • New RetryPolicy with fluent composition:
    • immediate()
    • fixed(Duration)
    • exponential(Duration)
    • .maxAttempts(n)
    • .maxDelay(Duration)
    • .withJitter(factor)
    • .retryWhen(Predicate<Throwable>)

✅ Runtime and Capability Improvements

  • DefaultEffectRuntime now implements AutoCloseable
  • Async and fork execution now use trampolined interpretation for stack safety
  • Replaced spin-wait startup synchronization with CountDownLatch
  • Improved capability dispatch semantics and handler composition behavior

🛠️ Fixes

  • Closed stack-safety gaps in runAsync and executeFork
  • Removed CPU-burning spin-wait loops in async/fork startup
  • Fixed capability composition fall-through behavior to avoid swallowing internal handler errors
  • Fixed interface-resolution edge cases in CompositeCapabilityHandler
  • Corrected docs drift where .retry() and .timeout() were previously documented but missing
  • Updated Java-idiomatic tuple accessor names (first(), second(), third())

🧪 Testing

  • 205 total tests (up from ~100 in 0.1.0)
  • New dedicated test coverage for:
    • combinators (tap, retry variants, timeout, conditional effects)
    • collection helpers (sequence, traverse, parAll, race)
    • Either enriched API
    • capability handler builder/composition
    • retry policy validation + integration
    • full Resource lifecycle semantics

📦 Installation

Maven

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

Gradle (Kotlin DSL)

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

Gradle (Groovy)

implementation 'com.cajunsystems:roux:0.2.0'

🔗 Links


Full Changelog: v0.1.0...v0.2.0

Roux 0.1.0 - Initial Release

16 Dec 08:59

Choose a tag to compare

Roux 0.1.0 - Initial Release

We're excited to announce the first release of Roux - a modern effect system for Java 21+ that brings composable, type-safe effects to the JVM!

🎉 What is Roux?

Roux is a lightweight, pragmatic effect system built from the ground up for Java 21+ virtual threads. It provides a clean, composable way to handle side effects while staying close to Java's natural behavior.

✨ Key Features

🧵 Virtual Thread Native

Built specifically for JDK 21+ virtual threads with structured concurrency support.

🎯 Core Effect System

  • Type-safe effects with explicit error channel: Effect<E, A>
  • Pure values - Effect.succeed() for wrapping values
  • Failures - Effect.fail() for explicit error handling
  • Lazy evaluation - Effect.suspend() for deferred computations

🔀 Rich Combinators

  • map - Transform success values
  • flatMap - Chain effects sequentially (monadic composition)
  • catchAll - Handle and recover from errors
  • mapError - Transform error types
  • orElse - Fallback to alternative effect on failure
  • attempt - Convert effect to Either<E, A>
  • fold - Handle both success and error cases
  • widen/narrow - Type-safe error type transformations

⚡ Concurrency & Parallelism

  • Fork/Fiber - Launch effects concurrently
  • join - Wait for fiber completion
  • interrupt - Cancel running fibers
  • zipPar - Run two effects in parallel
  • Effects.par - Run multiple effects in parallel

🏗️ Structured Concurrency

  • Effect.scoped - Create structured concurrency scopes
  • Automatic cancellation - All forked effects cancelled on scope exit
  • Built on Java 21's StructuredTaskScope (JEP 453)

🎭 Algebraic Effects (Capabilities)

  • Capability system - Define custom algebraic effects
  • CapabilityHandler - Handle capabilities with custom interpreters
  • Type-safe composition - Compose handlers safely

🎨 Generator-Style Effects

  • Effect.generate - Build effects using imperative-style generators
  • GeneratorContext - Imperative API for effect building
    • perform - Execute capabilities
    • yield - Embed effects
    • call - Execute throwing operations

🛡️ Stack Safety

  • Trampolined execution - Stack-safe by default
  • Constant stack depth - Handle millions of flatMap operations without stack overflow
  • Enabled by default - No configuration needed

📦 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);

📚 Documentation

🧪 Testing

This release includes 100+ unit tests covering:

  • All effect combinators
  • Stack safety (chains up to 1,000,000 operations)
  • Concurrency and fork/fiber behavior
  • Structured concurrency scopes
  • Capability system
  • Generator-style effects

🔗 Links

🙏 Acknowledgments

Roux is inspired by modern effect systems like ZIO and Cats Effect, adapted for Java's unique strengths with virtual threads and structured concurrency.

📝 License

MIT License - see LICENSE file for details.


Full Changelog: https://github.com/CajunSystems/roux/blob/main/CHANGELOG.md