Skip to content

Explore: user-extensible effects via CPS continuations (additive to HKT-emulation) #78

Description

@eschizoid

Context

This issue captures an idea raised by u/wernerdegroot in the r/java post discussion of telescope's 1.0 candidate. Wernerdegroot has built a similar Java optics library (guetta) and pointed out that his SetterWithEffect uses CPS continuations to give users their own effects with no library-side support — no HKT-emulation, no per-effect Applicative witness.

Reddit thread quotes:

I have one feature that you don't seem to have, which may pique your interest. As you know, Java does not support higher kinded types, so applicative traversal seems off the table. I discovered that you can get (monomorphic) applicative traversal using continuations. The traversal can then support optional/future/list traversal, and any other kind of effect through one method.

SetterWithEffect.java in guetta

The nice thing (I think) is that continuations allow the user to implement their own "control flow" (we call it a monad or an applicative). Optional of future? The user can build it easily. No HKT classes needed. The only really unfortunate side effect is that applicatives stack automatically (an applicative of an applicative is also automatically an applicative) but the continuation encoding is monadic in nature. So applicatives don't stack.

And SetterWithEffect composes with another in a one-liner, if that was what you were wondering.

Where telescope is today

Effectful update lives in internal/optics/Traversal#modifyF:

<F extends Kind.Witness> Kind<F, S> modifyF(
    Applicative<F> applicative,
    S source,
    Function<? super A, ? extends Kind<F, A>> fn);

Four *K witnesses ship in core/runtime/instances/CompletableFutureK, OptionalK, EitherK, ValidatedK. The public surface exposes them as four update* methods on Telescope. Adding a fifth effect (say Try, IO, Reader) means writing an Applicative<MyK> witness + a sibling update* method.

The exploration

Add a CPS escape hatch alongside the existing update* methods — something like:

public <R> R modifyCps(
    final S source,
    final Function<A, Continuation<R, A>> fn,
    final Function<S, R> done);

Goal: let a user define their own effect without writing an Applicative witness. Pulls the ergonomic win wernerdegroot is selling for the user-extensibility case, without ripping out the HKT substrate that already serves the four shipped effects.

Open questions

The design needs a beat before we commit:

  • Stacking. Continuations are monadic; they don't auto-stack the way applicatives do. If a user wants Optional<Future<X>>, the CPS path makes them write the stack explicitly. Is that an acceptable tax for the extensibility? Maybe yes — they had the same control-flow choice in their own monad anyway.
  • Composition through .then(...). Does the CPS path compose cleanly when the user chains Telescope.of(A.class).field(...).field(...).modifyCps(...)? Wernerdegroot's claim is yes — "composes in a one-liner." We need to prototype to confirm.
  • Overlap with the existing four. Do we keep the four update* methods and add modifyCps next to them? Or does modifyCps subsume them (likely no — applicatives still stack better than continuations for the common cases). Lean toward additive.
  • Type-safety story. The *K witnesses get static Applicative<F> typing. A CPS-style API would lose some of that — the user's continuation type carries the constraints. Cost-of-error matters.

Non-goals

  • Replacing the HKT substrate. The lattice's Traversal#modifyF stays, the four *K witnesses stay, the four update* methods stay. This is purely additive — one new public method for the power-user case.
  • Shipping a Continuation<R, A> framework. We don't build a category-theory library here; we add a small thunk type that's enough for the escape hatch.

Done when

A concrete proposal in this thread describing:

  1. The modifyCps (or equivalent) signature
  2. The Continuation type's minimum shape
  3. A prototype of one user-defined effect (e.g. Try) using only modifyCps, no *K witness
  4. A short comparison vs. the equivalent update* shape, with honest tradeoffs

Once we have that, we can decide whether to merge or close.

Links

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquestionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions