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:
- The
modifyCps (or equivalent) signature
- The
Continuation type's minimum shape
- A prototype of one user-defined effect (e.g.
Try) using only modifyCps, no *K witness
- A short comparison vs. the equivalent
update* shape, with honest tradeoffs
Once we have that, we can decide whether to merge or close.
Links
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 hisSetterWithEffectuses CPS continuations to give users their own effects with no library-side support — no HKT-emulation, no per-effectApplicativewitness.Reddit thread quotes:
Where telescope is today
Effectful update lives in
internal/optics/Traversal#modifyF:Four
*Kwitnesses ship incore/runtime/instances/—CompletableFutureK,OptionalK,EitherK,ValidatedK. The public surface exposes them as fourupdate*methods onTelescope. Adding a fifth effect (sayTry,IO,Reader) means writing anApplicative<MyK>witness + a siblingupdate*method.The exploration
Add a CPS escape hatch alongside the existing
update*methods — something like:Goal: let a user define their own effect without writing an
Applicativewitness. 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:
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..then(...). Does the CPS path compose cleanly when the user chainsTelescope.of(A.class).field(...).field(...).modifyCps(...)? Wernerdegroot's claim is yes — "composes in a one-liner." We need to prototype to confirm.update*methods and addmodifyCpsnext to them? Or doesmodifyCpssubsume them (likely no — applicatives still stack better than continuations for the common cases). Lean toward additive.*Kwitnesses get staticApplicative<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
Traversal#modifyFstays, the four*Kwitnesses stay, the fourupdate*methods stay. This is purely additive — one new public method for the power-user case.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:
modifyCps(or equivalent) signatureContinuationtype's minimum shapeTry) using onlymodifyCps, no*Kwitnessupdate*shape, with honest tradeoffsOnce we have that, we can decide whether to merge or close.
Links
internal/optics/Traversal.java,Applicative.java,Kind.java