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
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
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