Skip to content

Typed effects with env and v0.3.0 release#6

Open
contrasam wants to merge 57 commits intomainfrom
feat/typed-effects-with-env
Open

Typed effects with env and v0.3.0 release#6
contrasam wants to merge 57 commits intomainfrom
feat/typed-effects-with-env

Conversation

@contrasam
Copy link
Copy Markdown
Contributor

No description provided.

contrasam and others added 30 commits March 8, 2026 10:57
Adds PROJECT.md for two-part initiative: verified law tests (functor/monad/error/capability) and test utilities (assertions, TestRuntime, capability test doubles).

Creates PROJECT.md with requirements and constraints.
6-phase milestone: law infrastructure, functor/monad laws, error/capability laws,
fluent assertions, TestRuntime, and capability test doubles.

Initializes STATE.md and phase directories.
Single plan: EffectLawSupport utility with observational effect equivalence
assertions, plus LawInfrastructureTest smoke tests.
One plan: FunctorLawsTest (identity, composition) and MonadLawsTest
(left identity, right identity, associativity), each tested with
success, failure, and suspend effect inputs.
One plan: ErrorChannelLawsTest (catchAll identity, mapError identity,
mapError composition, attempt round-trip) and CapabilityHandlerLawsTest
(orElse identity, compose associativity). 14 tests total.
One plan: EffectAssert fluent chain + EffectAssertions entry point with
eager execution, 8 assertion methods (succeeds/fails/with/predicate/andReturn/andError),
handler overload, and 22-test validation suite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
contrasam and others added 23 commits March 10, 2026 18:49
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t factories

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 10 plan: horizontal and vertical Layer composition operators.
Covers widenError helper, and() (same input, merged output), andProvide()
(sequential build, both outputs retained), and LayerCompositionTest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 11: finalize on() intersection bound (C extends F & Capability<R>),
commit user's forType() changes, and write end-to-end LayerIntegrationTest.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Note: Java JLS §4.4 forbids additional bounds when the first bound is a
type variable (F), so the exact intersection `C extends F & Capability<R>`
does not compile. The existing `C extends F` already transitively enforces
the Capability bound since Builder<F extends Capability<?>>. The forType()
factory and Builder<F> generics from the user's changes are retained as-is.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the full Milestone 2 API: phantom types (Empty/With),
HandlerEnv, EffectWithEnv, Layer (succeed/fromEffect), horizontal
and vertical layer composition, API reference tables, and design notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CapabilityHandler.builder() marked @deprecated(since="0.3.0") — points
  to forType() and HandlerEnv.of()+and() for multi-family environments
- HandlerEnv.of() updated to use forType() internally (avoids deprecation
  warning in production code)
- CAPABILITIES.md: consolidate sections 6+7 into a single forType() section
- TYPED_EFFECTS.md: replace remaining builder() reference with forType()

All existing call sites continue to compile (forRemoval=false).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Milestone 1 (Phases 1-6): Effect Laws & Test Utilities — 11 algebraic
  laws, EffectAssertions, TestRuntime, TestCapabilityHandler
- Milestone 2 (Phases 7-11): Type-Safe Layer System — HandlerEnv,
  EffectWithEnv, Layer (succeed/fromEffect/and/andProvide), forType() API
- ROADMAP.md collapsed to two-line summaries with archive links
- STATE.md updated with all decisions and notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@contrasam
Copy link
Copy Markdown
Contributor Author

@greptileai can you review this PR for me?

2 similar comments
@contrasam
Copy link
Copy Markdown
Contributor Author

@greptileai can you review this PR for me?

@contrasam
Copy link
Copy Markdown
Contributor Author

@greptileai can you review this PR for me?

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR introduces the typed-effects-with-environment system for v0.3.0: a phantom-type layer (EffectWithEnv<R,E,A>, HandlerEnv<R>, Layer<RIn,E,ROut>, Empty, With<A,B>) that statically tracks which capabilities an effect requires, plus a virtual-clock TestRuntime/TestClock pair for instant sleep-based testing. Effect.sleep() is refactored from an opaque Suspend node into a first-class Sleep effect node, enabling runtime interception.

Key findings:

  • Breaking sealed-interface changeSleep is a new permitted subtype of the public sealed interface Effect. Any downstream code performing an exhaustive switch over all Effect variants will fail to compile without a Sleep branch. Custom EffectRuntime implementations require migration. This should be documented explicitly in v0.3.0 release notes.
  • HandlerEnv.of single-level interface walk — the dispatcher in CapabilityHandler.Builder.build() walks only capability.getClass().getInterfaces() one level deep. This correctly handles the standard flat sealed-interface pattern but silently fails with an UnsupportedOperationException for nested sealed hierarchies.
  • performSleep precision lossThread.sleep(duration.toMillis()) silently truncates sub-millisecond durations. Thread.sleep(Duration) (Java 19+) would preserve nanosecond resolution.
  • The EffectWithEnv, Layer, Empty, With, and TestRuntime/TestClock implementations are well-structured, well-tested, and correct for their stated design goals.

Confidence Score: 4/5

Safe to merge after addressing the Sleep sealed-type breaking-change documentation; the runtime logic itself is correct.

One P1 finding remains: adding Sleep as a new sealed Effect subtype is a source-breaking change for any code doing exhaustive pattern matching on Effect, and it is not yet documented in migration notes. The two P2 findings (sub-millisecond truncation, shallow interface walk) do not block merge.

lib/src/main/java/com/cajunsystems/roux/Effect.java (new sealed subtype), lib/src/main/java/com/cajunsystems/roux/capability/HandlerEnv.java (interface-walk depth), lib/src/main/java/com/cajunsystems/roux/runtime/DefaultEffectRuntime.java (sleep precision)

Important Files Changed

Filename Overview
lib/src/main/java/com/cajunsystems/roux/Effect.java Adds Sleep<E> as a first-class sealed Effect subtype and updates sleep() smart constructor to use it — source-breaking for exhaustive pattern matches on Effect.
lib/src/main/java/com/cajunsystems/roux/EffectWithEnv.java New phantom-typed wrapper EffectWithEnv<R, E, A> that statically tracks capability requirements; implementation is clean with correct map/flatMap/run semantics.
lib/src/main/java/com/cajunsystems/roux/capability/HandlerEnv.java New typed capability environment wrapper with phantom type R; of() dispatcher relies on single-level interface walk which silently breaks for nested sealed hierarchies.
lib/src/main/java/com/cajunsystems/roux/capability/Layer.java New Layer<RIn, E, ROut> abstraction for environment construction, supporting horizontal (and) and vertical (andProvide) composition; design is sound.
lib/src/main/java/com/cajunsystems/roux/runtime/DefaultEffectRuntime.java Adds Sleep handling in both direct and trampoline execution paths; introduces overridable performSleep for virtual-clock testing. toMillis() truncates sub-millisecond durations.
lib/src/main/java/com/cajunsystems/roux/capability/CapabilityHandler.java Adds typed forType(Class) factory and parameterises Builder<F>; deprecates untyped builder(). Functionally correct, though capabilityType parameter remains unused at runtime.
lib/src/test/java/com/cajunsystems/roux/testing/TestRuntime.java Clean DefaultEffectRuntime subclass that overrides performSleep to advance a virtual TestClock instead of blocking, enabling instant sleep tests.
lib/src/main/java/com/cajunsystems/roux/capability/Empty.java Minimal phantom-type marker interface for empty capability environments — correct and intentionally uninstantiable.
lib/src/main/java/com/cajunsystems/roux/capability/With.java Phantom type With<A,B> representing capability environment unions — correct and intentionally uninstantiable.

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    direction TB

    class `Effect~E,A~` {
        <<sealed interface>>
    }
    class `Sleep~E~` {
        <<record>>
        +Duration duration
    }

    class `EffectWithEnv~R,E,A~` {
        -Effect~E,A~ effect
        +of(Effect) EffectWithEnv
        +pure(Effect) EffectWithEnv
        +map(f) EffectWithEnv~R,E,B~
        +flatMap(f) EffectWithEnv~R,E,B~
        +run(HandlerEnv~R~, EffectRuntime) A
        +effect() Effect~E,A~
    }

    class `HandlerEnv~R~` {
        -CapabilityHandler~Capability?~ handler
        +of(Class, ThrowingFunction) HandlerEnv~C~
        +and(HandlerEnv~S~) HandlerEnv~With~R,S~~
        +fromHandler(CapabilityHandler) HandlerEnv~R~
        +empty() HandlerEnv~Empty~
        +toHandler() CapabilityHandler
    }

    class `Layer~RIn,E,ROut~` {
        <<functional interface>>
        +build(HandlerEnv~RIn~) Effect~E,HandlerEnv~ROut~~
        +succeed(Class, handler) Layer~Empty,RE,C~
        +fromEffect(Class, effectFn) Layer~RIn,E,C~
        +and(Layer) Layer~RIn,Throwable,With~ROut,S~~
        +andProvide(Layer) Layer~RIn,Throwable,With~ROut,S~~
    }

    class Empty {
        <<phantom interface>>
    }

    class `With~A,B~` {
        <<phantom interface>>
    }

    class DefaultEffectRuntime {
        +performSleep(Duration)
        +unsafeRun(Effect) A
        +unsafeRunWithHandler(Effect, handler) A
    }

    class TestRuntime {
        -TestClock clock
        +performSleep(Duration)
        +clock() TestClock
    }

    class TestClock {
        -AtomicLong virtualNanos
        +advance(Duration)
        +currentTime() Duration
        +reset()
    }

    `Effect~E,A~` <|-- `Sleep~E~`
    `EffectWithEnv~R,E,A~` --> `Effect~E,A~` : wraps
    `EffectWithEnv~R,E,A~` --> `HandlerEnv~R~` : run requires
    `HandlerEnv~R~` --> `Layer~RIn,E,ROut~` : produced by
    `HandlerEnv~R~` ..> Empty : uses
    `HandlerEnv~R~` ..> `With~A,B~` : uses
    DefaultEffectRuntime <|-- TestRuntime
    TestRuntime --> TestClock : delegates sleep to
Loading

Reviews (2): Last reviewed commit: "chore: archive Milestone 1 and Milestone..." | Re-trigger Greptile

@@ -0,0 +1,13 @@
# Roux v0.2.2
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Release notes version mismatch

The PR title is "Typed effects with env and v0.3.0 release", but the only release-notes file added is RELEASE_NOTES_0.2.2.md, which describes a v0.2.2 patch. There is no RELEASE_NOTES_0.3.0.md anywhere in this diff.

If this PR is intended as the v0.3.0 release, the significant new features — EffectWithEnv, HandlerEnv, Layer, phantom types (Empty/With), virtual-time TestRuntime, and the Sleep effect node — should all be documented in a dedicated RELEASE_NOTES_0.3.0.md.

Comment on lines +106 to +117
* Use with effects that have {@link Empty} requirements.
*
* <pre>{@code
* HandlerEnv<Empty> env = HandlerEnv.empty();
* }</pre>
*/
public static HandlerEnv<Empty> empty() {
CapabilityHandler<Capability<?>> noOp = new CapabilityHandler<>() {
@Override
public <R> R handle(Capability<?> cap) {
throw new UnsupportedOperationException(
"No handler registered for capability: " + cap.getClass().getName());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 HandlerEnv.empty() allocates a new handler on every call

A new anonymous CapabilityHandler instance is created each time empty() is called. Since the no-op handler is stateless, it can be a private static constant that is reused, avoiding needless allocation.

Suggested change
* Use with effects that have {@link Empty} requirements.
*
* <pre>{@code
* HandlerEnv<Empty> env = HandlerEnv.empty();
* }</pre>
*/
public static HandlerEnv<Empty> empty() {
CapabilityHandler<Capability<?>> noOp = new CapabilityHandler<>() {
@Override
public <R> R handle(Capability<?> cap) {
throw new UnsupportedOperationException(
"No handler registered for capability: " + cap.getClass().getName());
private static final HandlerEnv<Empty> EMPTY_INSTANCE = new HandlerEnv<>(
new CapabilityHandler<>() {
@Override
public <R> R handle(Capability<?> cap) {
throw new UnsupportedOperationException(
"No handler registered for capability: " + cap.getClass().getName());
}
});
public static HandlerEnv<Empty> empty() {
return EMPTY_INSTANCE;
}

Comment on lines +113 to +120
* sealed interface AppCapability<R> extends Capability<R> {
* record Log(String msg) implements AppCapability<Unit> {}
* record GetValue(String key) implements AppCapability<String> {}
* }
*
* var handler = CapabilityHandler.forType(AppCapability.class)
* .on(AppCapability.Log.class, c -> { logger.info(c.msg()); return Unit.unit(); })
* .on(AppCapability.GetValue.class, c -> "value-" + c.key())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 forType parameter is silently ignored at runtime

capabilityType is accepted but never read — it exists solely to drive compile-time type inference of F. While intentional, the compiler and IDEs will warn about the unused parameter. Adding @SuppressWarnings and a brief inline note would silence the warning and signal to future maintainers that the omission is deliberate:

Suggested change
* sealed interface AppCapability<R> extends Capability<R> {
* record Log(String msg) implements AppCapability<Unit> {}
* record GetValue(String key) implements AppCapability<String> {}
* }
*
* var handler = CapabilityHandler.forType(AppCapability.class)
* .on(AppCapability.Log.class, c -> { logger.info(c.msg()); return Unit.unit(); })
* .on(AppCapability.GetValue.class, c -> "value-" + c.key())
@SuppressWarnings("unused") // capabilityType is used only for compile-time type inference of F
static <F extends Capability<?>> Builder<F> forType(Class<F> capabilityType) {
return new Builder<>();
}

Comment on lines +139 to +143
* Layer<Empty, Throwable, With<DbOps, AuditOps>> appLayer = dbLayer.and(auditLayer);
* HandlerEnv<With<DbOps, AuditOps>> env =
* runtime.unsafeRun(appLayer.build(HandlerEnv.empty()));
* }</pre>
*
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant intermediate variable in fromEffect

result is assigned and immediately returned. It can be inlined:

Suggested change
* Layer<Empty, Throwable, With<DbOps, AuditOps>> appLayer = dbLayer.and(auditLayer);
* HandlerEnv<With<DbOps, AuditOps>> env =
* runtime.unsafeRun(appLayer.build(HandlerEnv.empty()));
* }</pre>
*
return env -> effectFn.apply(env).map(h -> HandlerEnv.of(type, h));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@contrasam
Copy link
Copy Markdown
Contributor Author

@greptileai i did not push some changes, pushed all the changes now

java.util.List<Effect<E, A>> effects
) implements Effect<Throwable, A> {}
/** Suspends execution for the given duration. Interpreted by the runtime. */
record Sleep<E extends Throwable>(Duration duration) implements Effect<E, Unit> {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sleep is a source-breaking sealed subtype addition

Sleep is added as a new permitted subtype of the public sealed interface Effect. Any consumer code that performs an exhaustive switch expression over all Effect variants will now fail to compile with a missing case error. For example:

// Previously compiled; now fails after upgrading
return switch (effect) {
    case Effect.Pure<E,A> p     -> ...
    case Effect.Fail<E,A> f     -> ...
    case Effect.Suspend<E,A> s  -> ...
    // ... (all prior cases) ...
    // missing: case Effect.Sleep<?> s -> ...  ← compile error
};

Since Effect is a public API and sealed interfaces enforce exhaustiveness at compile time, this is a source-breaking change. It should be called out explicitly in the v0.3.0 migration notes, noting that any custom EffectRuntime implementations must add a Sleep branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant