Skip to content

Add type-safe handler system with phantom-type layers#5

Open
contrasam wants to merge 2 commits intomainfrom
claude/type-safe-handlers-capabilities-ZthqE
Open

Add type-safe handler system with phantom-type layers#5
contrasam wants to merge 2 commits intomainfrom
claude/type-safe-handlers-capabilities-ZthqE

Conversation

@contrasam
Copy link
Copy Markdown
Contributor

Summary

Introduces a ZIO-style layer system for roux that uses phantom types to enforce compile-time verification that every capability an effect requires has a corresponding handler. This eliminates the possibility of runtime UnsupportedOperationException due to missing handlers — the program simply won't compile if capabilities are unhandled.

Key Changes

  • HandlerEnv<R> — A typed wrapper around CapabilityHandler where the phantom type parameter R tracks which capabilities are provided. Supports composition via and() to merge multiple capability environments.

  • TypedEffect<R, E, A> — A thin wrapper around Effect<E, A> that statically declares capability requirements via phantom type R. The run() method only compiles when the caller provides a HandlerEnv<R> with matching phantom type.

  • Layer<RIn, E, ROut> — A recipe for building a HandlerEnv<ROut> from a HandlerEnv<RIn>, with support for both horizontal composition (and() — merge outputs) and vertical composition (andProvide() — feed one layer's output into another's input).

  • Phantom type systemEmpty and With<A, B> form a type-level set of capabilities:

    • Empty represents no capabilities (base case)
    • With<A, B> represents both A and B capabilities
    • Nesting allows arbitrary numbers: With<A, With<B, C>>
  • F-bounded polymorphism — All factory methods (HandlerEnv.of, Layer.succeed, Layer.fromEffect) use F-bounds (<R, C extends Capability<R>>) to eliminate wildcards and ensure handler return types match capability declarations at compile time.

  • Comprehensive test suiteTypeSafeHandlerTest demonstrates single and combined environments, typed effect execution, layer composition (horizontal and vertical), and full end-to-end integration.

  • Documentation — New TYPED_LAYERS.md guide covering motivation, phantom types, all three abstractions, composition patterns, and design notes on how Java's approach differs from ZIO.

Implementation Details

  • HandlerEnv is a functional interface backed by the existing CapabilityHandler — zero runtime overhead
  • TypedEffect is a single-field wrapper around Effect — erased at runtime
  • Phantom types exist only in .class file signatures; no instances are ever created
  • All composition operations (and, andProvide) thread the phantom types correctly through the type system
  • Existing CapabilityHandler API remains unchanged; new types layer on top for opt-in type safety

https://claude.ai/code/session_01TBuJE3hwUJpjr2avujhXbL

claude added 2 commits March 10, 2026 08:34
Introduces four new types in the capability package that let the compiler
verify at build time that every capability an effect requires has a handler:

* Empty  — phantom type representing an empty (no-capability) environment
* With<A,B> — phantom type encoding the union of two capability requirements
* HandlerEnv<R> — typed wrapper around CapabilityHandler<?> whose phantom
  parameter R tracks which capabilities are provided; compose with .and()
* Layer<RIn,E,ROut> — recipe for building a HandlerEnv<ROut> from a
  HandlerEnv<RIn>, possibly performing effects during construction; supports
  horizontal (.and) and vertical (.andProvide) composition
* TypedEffect<R,E,A> — thin wrapper around Effect<E,A> that declares R as the
  required capability environment; .run(env, runtime) only compiles when the
  caller holds a HandlerEnv<R> that matches

TypeSafeHandlerTest covers: single-cap envs, and() composition, associativity,
TypedEffect combinators (map/flatMap), pure effects, Layer.succeed, horizontal
layer composition (++), vertical layer composition (>>> / andProvide where a
layer reads config during construction), end-to-end layer→env→typedEffect
integration, and interop escape hatches (fromHandler / HandlerEnv.fromHandler).

https://claude.ai/code/session_01TBuJE3hwUJpjr2avujhXbL
…S doc

All factory methods that previously accepted ThrowingFunction<C, ?> now use
an F-bound to capture the capability's declared result type R:

  <C extends Capability<?>>   →   <R, C extends Capability<R>>
  ThrowingFunction<C, ?>      →   ThrowingFunction<C, R>

Affected methods:
  CapabilityHandler.Builder.on()
  HandlerEnv.of()
  Layer.succeed()
  Layer.fromEffect()

The compiler now verifies that handler lambdas return the exact type R
that the capability family declares, with no wildcards at call sites.

Also adds docs/TYPED_LAYERS.md, a comprehensive guide to the typed layer
system (HandlerEnv, Layer, TypedEffect, Empty, With) with worked examples,
a ZIO comparison table, and notes on Java-specific trade-offs.
docs/CAPABILITIES.md updated with a pointer to the new guide.

https://claude.ai/code/session_01TBuJE3hwUJpjr2avujhXbL
contrasam added a commit that referenced this pull request Apr 18, 2026
…coped/fork/join

The previous docs taught the manual scoped+forkIn+flatMap+join pyramid as the
idiomatic pattern for parallel work. This was intimidating and incorrect — that
pattern is only warranted when you need the scope handle for conditional
cancellation.

Changes:
- STRUCTURED_CONCURRENCY.md Pattern 1: lead with Effects.par(), show
  Effect.effect() as the imperative alternative, relegate Effect.scoped to
  "when you need scope.cancelAll()" with a concrete example of when that's true
- STRUCTURED_CONCURRENCY.md Pattern 5: lead with Effects.parTraverse /
  parTraverseEither; manual fiber loop only for per-fiber control cases
- STRUCTURED_CONCURRENCY.md Best Practices: add decision table, "avoid nesting
  pyramid" rule, and "when to use scoped directly" guidance; renumber to fit
- EFFECT_API.md Effect.scoped section: add tip box pointing to Effects.par for
  the common case; update example to show conditional cancelAll (the actual
  reason to reach for scoped)
- EFFECT_API.md Parallel Workflow: expand to show par, parTraverse, and
  Effect.effect() side by side
- EFFECT_API.md Resource Management: replace nested scoped example with
  Resource.make + Resources fluent builder
- EFFECT_API.md Best Practices: update #2 and #5 to point to higher-level tools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants