Unreleased: 0.6.0

Not released yet: these changes are on main, and the latest book describes them.

To try them, depend on 0.6.0-SNAPSHOT from the snapshots repository, as Gradle SNAPSHOT Configuration shows.

At a glance

  • javac checks what each step of a Path chain returns. A step of another kind of Path, or one that fails with an error the chain does not carry, is a compile error in the IDE as well as the build.
  • @ImportOptics pairs a JavaBeans-spelt wither with its getter: a witheMail beside geteMail() gives an eMail() lens.
  • A @GenerateFocus message that offers a navigator asks first for the renames it needs. Following its fix line no longer leads into another note or error.
  • A @GenerateMapping bean refusal's fix line compiles once followed, and names the near miss it passed over, such as setx(int) beside Integer getX().
  • A timeout cancels the task it timed out, as a Scope's timeout does, so a retried timeout leaves no attempt running.

Mapping

  • A bean property's fix line compiles once followed (#1149). The setter offered for eMails is seteMails, not setEMails. A rename to a keyword such as default is offered no marker, and a marker replaces the spec's own rename. A near miss such as setx(int) beside Integer getX() is named. See When an unpaired accessor is refused.
  • A mapping over a deprecated type compiles under -Werror (#1381). A deprecated v1 DTO, a bean's deprecated getter or a protobuf-java field declared deprecated drew warnings inside the generated Impl, where your own @SuppressWarnings cannot reach. A @GenerateMapping or @GenerateMerge Impl that names or calls a deprecated element now suppresses deprecation, or removal, on its own class. See Build-time impact.
  • Two more PATCH-bean fix lines compile once followed (#1293). A legacy setter such as setTimeout(Integer) beside a rename of timeout is offered no @Unmapped marker, since the rename takes that name. A primitive word such as setx(int) is offered its wrapper, setX(Integer). See A method that only starts like an accessor.
  • A mapping keeps every entry of an IdentityHashMap (#1483). A same-typed Map component's copy, and ValidatedPrism's parseValues and buildValues, rebuilt it as a LinkedHashMap, merging keys that were equal but not the same object. An IdentityHashMap now comes back one, with every key, as a map optic's result does. See Same-typed containers cross as copies.
  • A nullness checker can check a generated mapping's copies and guards (#1431). Its copy helpers, parse guards and leaf cache took or held a null without declaring it, so NullAway, run over a @NullMarked package, failed inside the Impl. Each now declares its nulls. See Same-typed containers cross as copies.
  • The processor catches a @Nullable component that build would copy into one declared non-null (#1431). build wrote the domain's null into a record component, setter or builder setter declared non-null, as a plain merge did into its target. Mark the site @Nullable, or declare the source non-null. See A @Nullable component is not copied into a non-null one.

Optics

  • Optics over a deprecated type compile under -Werror (#1375). A record with a HashBag component, deprecated in commons-collections4 4.6.0, or any component of a deprecated type, drew warnings inside the generated lenses, traversals and focus classes, where your own @SuppressWarnings cannot reach. Each generated member that names a deprecated class now suppresses deprecation, or removal, itself. See Build-time impact.
  • Traversals over commons-collections4's HashMultiSet (#1379). Apache's commons-collections4 4.6.0 deprecates HashBag and recommends HashMultiSet in its place. A HashMultiSet component now gets a traversal, as a HashBag one does, where it drew the note for a container no generator supports. See Apache Commons Collections.
  • @ImportOptics pairs a JavaBeans-spelt wither with its getter (#1148). A witheMail(String) beside geteMail(), as Lombok's beanspec capitalisation writes them, gives an eMail() lens, as withEMail beside getEMail() does. The processor says why it refuses a class whose only wither is with2D(int). See Wither Classes to Lenses.
  • A spec lens reads a getter inherited from two interfaces as javac does (#1136). An abstract source that inherits String name() from one interface and CharSequence name() from another is now read as returning String, so a Lens<Src, String> over it builds. A @ViaBuilder chain reads an inherited toBuilder() or build() the same way. See Compiler Messages.
  • Messages that offer a navigator ask first for the renames it needs (#1272). The note to widen items beside an Items with a navigator, and five other messages, now ask for one of the two to be renamed first. They also name a field such as last that the navigator could not reach. See Two fields with one navigator name.
  • An unknown @GenerateFocus field-list name is offered only a field the list can use (#1283). It no longer offers a name the list already holds. includeFields is offered a field that can have a navigator, widenFields one it can widen, and any rename the field's navigator needs comes first. A name written twice in one list warns. See Compiler Messages.
  • OpticOpFunctor.map applies its function (#1154). It returned the instruction unchanged, so code that mapped a lifted OpticOp failed later with ClassCastException. Each OpticOp record now carries a continuation k, as an @EffectAlgebra operation does, and an interpreter hands each result to op.k() in place of wrapping it in Free.pure. See What an interpreter actually is.
  • Generated optics over a sorted collection keep its comparator (#1430). Traversals and Focus paths over PSortedSet, PSortedMap and the Eclipse sorted sets rebuild with the source's comparator, so a custom order is kept. The …Rebuilding factories on Traversals and EachInstances do the same for your own optics, and getOrderedTypeArgumentIndex() for your own generators. See Sorted Variants and Comparators.
  • An IdentityHashMap written through an optic keeps every entry (#1458). Each optic that writes a map rebuilt it as a LinkedHashMap, so two keys that were equal but not the same object became one, and an entry was lost. An IdentityHashMap now comes back one, with every key. See The forMapValues() Traversal.
  • StreamTraversal.toTraversal() works over a VStream (#1442). On forVStream(), or a composition built on it, the result read its source twice, so a fromIterator source came out empty, and threw NoSuchElementException when read again. It now reads the source once, and gives a stream that replays the results. See Converting to a Traversal.
  • AffinePath.ofNullable takes a getter that never returns null (#1488). Under a nullness checker it took only a Function<S, @Nullable A>, so a Function<S, String> variable was refused. It now takes Function<? super S, ? extends @Nullable A>.

Effect Paths

  • javac checks each chain step's kind of Path and error type (#1314). A step passed to via or another chaining method must be the chain's own kind of Path, so a mismatch is a compile error rather than an exception at run time. A step may still fail with a variant of the chain's error. See The Family Parameter.
  • EitherPath gains ensure (#1338): ensure(test, onFail) keeps a success whose value passes test and turns any other success into a failure built from its value, so a guard no longer needs an if returning a Left. See EitherPath.
  • TrampolinePath and FreePath combine three paths with zipWith3 (#1302): both already combined two paths with zipWith. The three-way combine stays lazy until run() or foldMap, so a deep chain stays stack-safe. See Combinable (Applicative).
  • NonDetPath gains six list operations that ListPath already had (#1337): lastOption, anyMatch, allMatch, takeWhile, dropWhile and get. Each behaves as its ListPath counterpart.
  • NonDetPath gains foldRight, fold and foldMap (#1322): with foldLeft, it now has the four folds that ListPath and StreamPath offer, and each behaves as its ListPath counterpart. See the Effect Path Cheatsheet.
  • StreamPath and NonDetPath gain toListPath() (#1152): ListPath, StreamPath and NonDetPath now each convert to the other two.
  • Monoid.of builds a monoid from its empty value and its operation, and Monoids.bigDecimalAddition() sums money exactly (#1356). Monoid.of(Duration.ZERO, Duration::plus) takes the place of an anonymous class, and an existing Semigroup can be the operation. See Semigroup and Monoid.
  • Maybe.fromEither converts an Either to a Maybe (#1405): a Right becomes Just with its value, while a Left becomes Nothing, discarding the left value.
  • Either gains fromOptional (#1406): converts a present Java Optional to Right and an empty one to Left, accepting either a direct error value or a supplier for the error.
  • Either gains orElse and orElseGet (#1407). orElse(other) returns a Right's value or other, and orElseGet(recovery) builds it from the error. A null argument is refused on a Right and on a Left, and a recovery that returns null is refused on a Left. A path uses getOrElse, because orElse there tries another path. See Reading an Either.
  • Either gains toOptional and swap (#1404). toOptional() keeps a Right's value, and gives an empty Optional for a Left. swap() turns a Left into a Right and back, as EitherPath.swap() does.
  • A fold can return null to a caller whose result type is nullable (#1433). fold on Either, Validated, EitherOrBoth, EitherF, Try, EitherPath, ValidationPath, EitherOrBothPath and TryPath, and the deprecated foldFailureFirst on Try and TryPath, take a @Nullable result type, so a nullness checker accepts parsed.<@Nullable Integer>fold(_ -> null, n -> n). See Values, never null.
  • VResultPath runs in one call with unsafeRun() and runAsync(), as VTaskPath does (#803). unsafeRun() returns the Either that run().run() gave, and runAsync() starts the task on a virtual thread and returns a future of it. A typed failure still arrives as a Left, and only an exception the task throws escapes.
  • Try and TryPath gain fold, failure first (#546), as Either.fold, Validated.fold and their Paths' fold already are. foldFailureFirst, which takes the same two functions in the same order, is deprecated for removal in 0.7.0, and the MigrateDeprecationsTo0_7_0 recipe renames each call. A Try traversal's generated code calls fold. See Reading a Try and Deprecated for removal in 0.7.0.
  • ListPath and NonDetPath name the null they refuse (#1438). A null element, or a map, zipWith or zipWith3 function that returns null, threw a NullPointerException with no message. The message now says which, as StreamPath's does.
  • A Selective of your own runs only the branch its condition picks (#1358). The default whenS ran its effect for a false condition, and the default ifS ran both branches. Both now leave the choice to select: one that keeps its contract skips what the condition does not pick, and one built from ap still runs every effect. See Selective.
  • mapError on MaybePath, TryPath and CompletableFuturePath carries the mapped error (#1332): recover after it sees that error, where it used to see the old one. A success is a Right and the mapper is not called. MaybePath and TryPath return an EitherPath. CompletableFuturePath returns a VResultPath. See Recoverable (MonadError).
  • toEitherPath, toMaybePath and toTryPath follow recover on a failed CompletableFuturePath (#1354). A cancelled future gives Left of its CancellationException, or Nothing, instead of throwing. toTryPath holds the exception the future failed with, not a CompletionException around it.
  • A half-open CircuitBreaker admits a call only while running and successful probes are fewer than successThreshold (#1481); it admitted every call. Any other call gets a CircuitOpenException, as when the circuit is open. A call now counts only towards the state it was admitted in, so a success from before an outage cannot close the circuit. See Circuit Breaker.
  • Each attempt at a call that one CircuitBreaker guards twice counts once, at the guard nearest the dependency (#1482). A repository and the service that calls it, each guarding one dependency with one breaker, counted every outcome twice. The inner guard now counts each attempt, and an open circuit turns a retry's later attempts away. See The State Machine.
  • A VStream's recoverWith, mapError and onError act on a failure at any element (#1029). They acted on the first pull only, and so did VStreamPath's. A later recover still resumes from the element after the failure. See VStream.
  • A VStream's recover after a failed mapTask carries on through the operators between them (#1029). The elements after the failure skipped those operators, so a filter let them through, and a map or zipWith handed on elements of the wrong type. A failed element is now a Step.Fail, which each operator passes on. See When an element fails.
  • One exception instance thrown at several elements, or by several VStreams, no longer mixes them up (#1029). A recover could go back to an earlier element or into another stream, and each failure added to the instance's suppressed exceptions. A failed element now holds the rest of its own stream, so nothing is added for it.
  • chunk, chunkWhile, mapChunked and the parEvalMap operators hand on what they read before a failure (#1029). They dropped it, so a forEach after them missed elements that a forEach without them saw. An interrupt still ends the stream at once.
  • A recover after parEvalMap, parEvalMapUnordered or parEvalFlatMap carries on past a failed task (#1437). The task's failure ended the stream, so swapping mapTask for parEvalMap lost recovery. Its element is now a failure in its place, as after mapTask, and the other tasks run on. See When a task fails.
  • parEvalFlatMap closes the streams it built but never reached (#1454). When a take or headOption stopped inside one of a batch's streams, or a failure or an interrupt ended the stream, the rest of the batch's streams were dropped unclosed, and their finalisers never ran. Closing the stream now closes them.
  • VStream.closeAfterFailure is deprecated for removal in 0.7.0 (#1029). A failed element now holds the rest of the stream, so a pull that throws leaves nothing that stream.close() misses. See Deprecated for removal in 0.7.0.
  • A VStream fails with the exception its task threw (#1410). A checked one, such as an IOException from mapTask, a finaliser or a publisher, reached every terminal operation inside a VTaskExecutionException, or a RuntimeException from parEvalMap, parEvalMapUnordered and merge. run(), and the conversions that call it such as toListPath(), still wrap it.
  • Par, Scope and VStreamPar fail with the task's own Throwable (#1418). A Throwable that is neither an Exception nor an Error reached the caller inside a RuntimeException once the task was forked, though the task run on its own failed with it. VTask.asCallable() now throws it unchanged.
  • Instances.alternative(vstream()) gives VStream's Alternative (#1425). It failed with a ClassCastException, because the vstream() token held a plain monad, so code had to name VStreamAlternative.INSTANCE. Instances.monad(vstream()) now returns that same object, which behaves as the monad did.
  • VStreamPar.parEvalMapUnordered emits each result as its task completes (#1419). It waited for each batch's slowest task, then emitted the batch in input order. Now a slow task holds back only its own result. Its running tasks and waiting results are never more than concurrency together, so a slow consumer slows the reading. VStreamPath's does the same.
  • VStreamPar.parCollect is deprecated for removal in 0.7.0 (#1427), with VStreamPath.parCollect and PathOps.parCollectVStream. A stream is read one pull at a time, so they run nothing in parallel and give the list toList() gives. Put work you want run in parallel in parEvalMap's function, then call toList(). The MigrateDeprecationsTo0_7_0 recipe rewrites each call. See Deprecated for removal in 0.7.0.
  • A VTaskPath hands every handler the exception its task threw (#1396). A checked one, such as an IOException, reached runSafe, catching and toTryPath, and any step after via, then or guarantee, wrapped in a VTaskExecutionException, so a retry on IOException gave up at once. Only unsafeRun(), which cannot throw one, still wraps it. See VTaskPath.
  • Context.recoverWith recovers from a failure in an earlier map or flatMap function (#1443). An exception thrown by the function, or a function that returned null, escaped a recoverWith later in the chain, though recover and mapError caught it. A recovery function that fails now hands its failure to the next recoverWith out.
  • A timeout cancels the task it timed out, as a Scope's timeout does (#1372). When the time runs out, VTask.timeout interrupts the task and fails once it has stopped, so a retry over a timeout leaves no attempt running. Every withTimeout but CompletableFuturePath's builds on it, as do ResilienceBuilder and the circuit breaker's callTimeout. See VTaskPath.
  • detachOnCancel() lets a timeout fail on time over work that cannot stop at once (#1372). The work is still interrupted, but nothing waits for it, so a call blocked where an interrupt cannot reach it still fails at the deadline, and a circuit breaker can open. It is on VTask, IOPath, VTaskPath, VResultPath and VTaskContext. See Timeouts.
  • A fromPublisher stream can be read again, and asks only for what it can hold (#1442). It subscribed once, when built, so a second read waited for ever, and asked for more as each element arrived, however full its buffer. Each run now subscribes at its first pull, and asks for one more per element taken. See Publisher to VStream.
  • A long State chain built with map or ap no longer overflows the stack (#1448). Each map and ap step called the step before it, as WithStatePath's peek, zipWith, zipWith3 and then did, so tens of thousands of them threw StackOverflowError. They now run in the loop flatMap already used. See State.
  • Natural is the library's one natural-transformation type (#1455). GenericPath.mapK takes a Natural, and VStreamTransformations gives Naturals. The effect package's NaturalTransformation now extends Natural, and is deprecated for removal in 0.7.0 with FreePath.foldMapWith and FreeApPath.foldMapWith. The MigrateDeprecationsTo0_7_0 recipe rewrites each use. See Deprecated for removal in 0.7.0.
  • A VStream terminal operation stops at an interrupt (#1428). toList, fold, drain and the others did not check between pulls, so neither a timeout nor a cancelled Scope could stop a stream whose pulls never block. Each now fails with an InterruptedException and closes what it has read, so the finalisers there run. See Running a stream.
  • Stopping one run of a shared VStream leaves its other runs alone (#1461). A take(0), a failed first pull or a close() on a stream another thread was reading released that thread's bracket resource, ran its onFinalize finaliser, or stopped its merge, fromPublisher or parEvalMapUnordered. Each run now closes only what it opened. See bracket opens and closes.
  • Free.foldMap keeps what each step's value holds besides its result (#1460). For a target that runs each step at once, such as Writer or List, it kept only the result. A Writer or WriterT<Id, ...> lost its log, a List its other elements and an EitherOrBoth its warnings. Each value now reaches the target whole. See Free Monad.
  • Free.foldMap runs a deep program into Id, Either or Writer without overflowing the stack (#1460). A recursive program of under a thousand steps overflowed it. Into IO or State, a deep program already ran. A List step that gives several results still adds to the stack, and so does each step of a Reader when it runs.
  • A Free program over instructions with no Functor of their own can now be run, through Coyoneda (#1470). Coyoneda.foldMap(natural, functor) runs a held value through an interpreter, and Coyoneda.interpreter(natural, functor) gives the interpreter that a FreePath needs when its Functor is CoyonedaFunctor. See Coyoneda.
  • A long chain of map calls on a Coyoneda no longer overflows the stack (#1470). Each map wrapped the function before it, so lowering ten thousand of them threw StackOverflowError. The functions are now applied one after another in a loop. See Coyoneda.
  • A long FreeAp program no longer overflows the stack, and its interpreter is handed the instructions in the order they were written (#1470). A fold of ten thousand map2 or zipWith calls threw StackOverflowError from foldMap, and so from FreeApAnalyzer.analyseWith, which runs through it. The interpreter was also handed the instructions in reverse order. See Free Applicative.
  • Every Path's getOrElseGet refuses a supplier that returns null (#1489). On MaybePath, EitherPath, ValidationPath, EitherOrBothPath and OptionalPath, and in Maybe.orElseGet, a null from the supplier was handed back, though the result type is non-null. It now throws NullPointerException, as TryPath.getOrElseGet and Either.orElseGet already did.
  • OptionalContext.io and JavaOptionalContext.io take a supplier that never returns null (#1488). Under a nullness checker they took only a Supplier<@Nullable A>, so a Supplier<String> variable was refused. They now take Supplier<? extends @Nullable A>.
  • Stop a runAsync() task by cancelling its future (#1412). cancel(true) on the future from runAsync() on VTask, VTaskPath, VResultPath, VTaskContext or VStream, or from EffectBoundary.runAsync, interrupts the task, as a future from an ExecutorService does, and cancel(false) leaves it running. See Running a task.

Spring

  • A VTaskPath response timeout interrupts its task (#1412). When hkj.virtual-threads.default-timeout-ms runs out, the starter answers 504 and interrupts the task, without waiting for it to stop. The task used to run on after the response had gone. See VTaskPath.

Testing


Build and tooling

  • The HKJ checker names the fix beside javac's error for a mismatched step (#1314). error-type-mismatch says where to type the chain, as in write .<OrderError>mapError(...), or that the step's error needs mapping. path-type-mismatch now reads method-reference steps, VResultPath and the accumulating methods. See Compile-Time Checks.
  • A build with an HKJ plugin applied can set the checker's options (#1336). A hand-written -Xplugin:HKJChecker severity=warn failed the build with plug-in not found: HKJChecker. The Gradle and Maven plugins now add no checker argument when the build already names the checker. See Configuring the checker.
  • A build with the HKJ Gradle plugin can turn on Gradle's configuration cache (#1374). With the cache on, hkjInstallSkills and hkjDiagnostics failed, and so did every build with skills = true, which runs hkjInstallSkills. Both tasks now run with it, and the next build reuses the cache. See With the Plugin.
  • Installed Claude Code skills say which release they describe, and a build keeps one copy (#1373). Each install writes only what changed, and .claude/skills/.hkj-skills-version. A build says when the stamp names another release, or is missing, as on every earlier copy. Copies in modules stay, and each module says so, until you remove them.
  • One hkj-openrewrite release takes a 0.4.x project's Try.fold calls to 0.6.0 (#546). The swap in MigrateDeprecationsTo0_5_0 now leaves 0.6.0's fold alone, since it swaps only a fold declared with the success function first. So run the 0.5.0 group before the move and the 0.7.0 group after, both from 0.6.x. See 0.5.0 deprecation migration.
  • An OpenRewrite recipe adds the Path family to capability types (#1314). org.higherkindedj.openrewrite.AddPathFamily rewrites Chainable<A> to Chainable<?, A>, and likewise the other capability types, a PathProvider's createPath included. See 0.6.0 Path family.
  • javadoc runs in a build with the HKJ Gradle plugin applied (#1385). With preview features on, the default, javadoc received --enable-preview without the release it needs, so javadoc failed, and so did build in a project that publishes a javadoc jar. The plugin now passes --release as well, unless the task passes its own -source or --release. See Defaults.
  • The benchmark options the docs describe now work (#1439). The benchmarks read only -Pincludes, so -Pjmh.iterations, -Pjmh.fork, -Pjmh.profilers=gc and the rest did nothing, and the --includes most pages gave failed. The build now reads each one, and Benchmarks lists them.
  • A generator plugin needs only hkj-processor from HKJ (#1376). BaseTraversableGenerator moves there under the same name, and javapoet, whose types the generator interface uses, comes with it. A plugin's build can drop a javapoet line it lists by hand, or an hkj-processor-plugins line it added to reach the base class. Its module-info.java keeps requires com.palantir.javapoet;. See Step-by-Step Example.
  • The 0.5.0 migration marks more of the nulls 0.5.0 refuses (#1370). Its new DetectPossibleNullSuccessValuesRecipe marks a @Nullable value passed on to a success factory, and a @Nullable method read by Try.of or a map. It marks a null check in a later step, which 0.5.0 never hands a null. A 0.5.0 project can run it alone. See 0.5.0 deprecation migration.
  • MigrateDeprecationsTo0_5_0 puts a lambda's mark on the null it returns (#1370). The mark sat at the start of the call's chain, which could be many lines before the null. See 0.5.0 deprecation migration.

Documentation

  • StreamPath docs no longer call it lazy where the source is read at once (#1290). The cheat sheet and home page describe sequences you can run more than once, and toStreamPath says the non-null focuses are collected into a list when it is called.
  • The StreamPath docs say when the source is read (#1278). Path.stream reads its whole stream when it is called, so the stream must be finite. Path.streamIterate, StreamPath.generate and StreamPath.fromSupplier build a fresh stream each time the path runs, so take can limit an infinite one. See Choosing the Right Streaming Tool.
  • The Effect Paths glossary gains three entries (#1301): FreeApPath, FreePath and GenericPath. Each gives a definition, its factories on Path, and its main operations. See Effect Paths.
  • The Effect Paths glossary gains four more entries (#1258): ReaderPath, TrampolinePath, WithStatePath and WriterPath. Each gives a definition, its factories on Path, its main operations and a link to its chapter page. See Effect Paths.
  • The Effect Paths glossary gains the four sequence Paths (#1263): ListPath, NonDetPath, StreamPath and VStreamPath. Each gives a definition, its factories on Path, its main operations and links for further reading. See Effect Paths.
  • Five examples keep one kind of Path and one error type through their chains. Composition Patterns ran an IOPath step in an EitherPath chain, which throws when it runs; it now builds an IOPath chain. The Quickstart, a migration recipe and the hkj-guide and hkj-spring skills now name the chain's error type with a witness. See Composition Patterns.
  • Generated Javadoc uses an article only before a type the processor chose (#1307). A record, class, property or operation is named with none, as in field of {@link Address} and Lifts the {@code Charge} operation. A path or optic type keeps the article its name takes, as in Creates an {@link AffinePath}.
  • The hkj-guide skill turns guard clauses into ensure (#1338). A new migration recipe replaces a run of if (!ok) return Either.left(...) checks with one ensure step for each, and the skill's Error Handling table lists ensure.
  • The Profunctor javadoc states its composition law correctly (#1362). Two of its functions were swapped, so the law as written fails. It no longer says profunctors model the optics, which are van Laarhoven optics, and Bifunctor's javadoc names Function as the only Profunctor instance. See Profunctor.
  • The VTask javadoc says which thread runs a task (#1396). run() runs it on the calling thread, and runAsync(), Par and Scope run tasks on virtual threads. It said every task ran on a virtual thread, as did the VTaskPath page and two glossary entries.
  • You can have NullAway flag a null returned through VTask.of or Try.of (#1371). In JSpecify mode alone, NullAway never checks what a lambda returns when passed as a JDK Supplier, Callable or BiFunction, as to VTask.of, Try.of or IO.delay, so the computation fails only when it runs. Two NullAway options make it check them. See What a nullness checker sees.
  • You can learn the type classes from one tested example. Most Foundations pages and Tutorial 00 use the order service the Optics and Mapping chapters share. Each core type's page opens with a card saying when to reach for it, Supported Types is rebuilt around those cards, and a new page lists where an instance bends a law. See Foundations.

Upgrading from 0.5.0

Before you upgrade

  • From 0.4.x, run MigrateDeprecationsTo0_5_0 while still on 0.4.x (#546). fold returns on Try and TryPath, failure first. A success-first fold left from 0.4.x compiles again where both functions accept either argument, and runs them swapped: t.fold(v -> true, e -> false) now gives true for a failure. Rebuild what was compiled against 0.4.x too. See To 0.6.0.
  • Expect the HKJ skills at the root of your build (#1373). The Gradle plugin, and mvn hkj:install-skills, put a copy in each module. Both now install once, at the root, and leave the module copies alone. Each module that holds one says so until you install at the root and remove it.

What a running program can notice

Mapping

  • A mapping refuses a map holding two keys that equals cannot tell apart, unless it is an IdentityHashMap (#1483). A same-typed Map copy, parseValues and buildValues lost an entry; they now throw. Hold such a map as an IdentityHashMap, which comes back a writable one, so look its keys up by the same object. See Same-typed containers cross as copies.

Core types

  • Validated.map, and map2 to map5 on its monad, throw NullPointerException when their function returns null (#1318). They threw KindUnwrapException, so catching that, or IllegalArgumentException, around the call no longer catches it. See Values, never null.
  • FunctionKindHelper.narrow throws KindUnwrapException, as the other kind helpers do (#1318). A null Kind threw NullPointerException, so catching that no longer catches it. Another type's Kind threw IllegalArgumentException, which KindUnwrapException extends, so catching that still does. See When narrow throws.
  • A Selective of your own no longer runs the effects its condition skips (#1358), unless its select is built from ap. The default whenS ran its effect on a false condition, and the default ifS ran both branches, so a failing effect's error could hide the condition's. Run any effect you relied on yourself. The library's own instances are unchanged.
  • Context.recoverWith now catches a failure from an earlier map or flatMap function (#1443). Code that let such a failure escape a recoverWith gets the recovered value instead, and a recovery function's own failure reaches the next recoverWith out. A checked exception such a function throws, as Kotlin code can, reaches run() inside a ContextException, as one from fail does.
  • Free.foldMap into a Writer, List or EitherOrBoth gives what each step's value holds (#1460). A Writer's log is no longer empty, and an EitherOrBoth that warned gives Both, where it gave Right. A List target runs the rest of the program, and its interpreter, once for each element a step gives.
  • Const and the optics' folds refuse a null value (#1470). new Const<>(null) throws NullPointerException, as does a mapFirst or bimap function returning null. A traversal's fold fails where its function or monoid gives null, where before the monoid got the null. A side-effect fold needs a Monoid<Unit>, not Monoid<Void>. See Values, never null.
  • A FreeAp interpreter is handed the instructions in the order the program was written (#1470), not in reverse. An interpreter that acts as it is called, such as one into Id that logs or calls a service, now does so in that order. Results, and the failures Validated accumulates, were already in order.

Optics

  • Of two withers for one getter, @ImportOptics sets through the capitalised spelling (#1148): the lens for a class with both withEMail and witheMail beside eMail() set through whichever the class declared first. It now sets through withEMail, and a note names witheMail, which a spec interface's @Wither can choose.
  • An optic interpreter that wraps each result in Free.pure skips the operation's continuation (#1154): a program from OpticPrograms runs as before, but a function mapped onto an OpticOp, through OpticOpFunctor or Free.translate, is not applied. Hand the result to op.k() and return what it gives.
  • A sorted collection keeps its order through a generated optic (#1430). A traversal over a PSortedSet or PSortedMap, and a Focus path over those or an Eclipse Collections sorted set, gave back a custom-ordered source in natural order. It now keeps the source's order. Where code relied on the re-sort, sort the result itself.
  • A map optic refuses any map but an IdentityHashMap holding two keys that equals cannot tell apart (#1458), such as a HashMap whose key changed after it went in. Writing one lost an entry; it now throws IllegalArgumentException. Copy such a map into an IdentityHashMap first. An IdentityHashMap comes back one, so look its keys up by the same object.
  • A null optic, modifier or predicate fails when the program is built (#1315). Each OpticOp record throws NullPointerException, and so does an OpticPrograms factory that builds one. It used to fail only when an interpreter ran the program. The validating interpreter no longer reports one as a failed operation. set and setAll still accept a null new value.

Effect Paths

  • A ValidationPath keeps its own semigroup through a step (#1314): the result of via, then, recoverWith and orElse accumulated with the step's semigroup, and now accumulates with the receiver's. After mapError, whose placeholder semigroup throws, a later accumulation now throws even past a step that had a semigroup; use mapErrorWith instead.
  • An EitherOrBothPath keeps its own semigroup through a recovery (#1314): the result of recoverWith and orElse accumulated with the recovery's semigroup, and now accumulates with the receiver's, as via and then already did.
  • mapError on MaybePath, TryPath and CompletableFuturePath applies the mapper (#1332): it used to hand back the same path, and it never called the mapper. A failure now runs the mapper, and recover sees that error. MaybePath and TryPath return an EitherPath. CompletableFuturePath returns a VResultPath. mapException still stays a TryPath.
  • toEitherPath and toMaybePath on a cancelled CompletableFuturePath no longer throw (#1354). They give Left of the CancellationException, and Nothing. toTryPath on a failed future holds that future's exception. Code which caught a CancellationException around toEitherPath or toMaybePath, or matched a CompletionException in a toTryPath failure, no longer sees one.
  • A VStream's recoverWith, mapError and onError now handle a failure at a later element (#1029). A failure at the second element went straight through them, so a fallback stream, an error mapper or a logging hook now runs where it did not. recoverWith, and recover when it ends the stream, close what they stop reading.
  • A VStream's recover after a failed mapTask passes the elements after the failure through the operators between them (#1029). A map, filter or take there now applies to them. A recover after VStreamPar.merge of several streams ends the stream there, and one after a task whose exception has suppression disabled carries on.
  • A failed mapTask element holds its place in a VStream (#1029). drop and dropWhile drop it within the prefix they drop, so a failure there no longer fails the stream. zipWith pairs it with the other stream's element and ends with the shorter stream, and throttle and metered pace it.
  • chunk, chunkWhile, mapChunked and the parEvalMap operators hand on what they read before a failure, then fail (#1029). A batch function or task now runs for those elements, with or without a recover, so a forEach or headOption after them sees them where it saw only the failure.
  • A failed task in parEvalMap, parEvalMapUnordered or parEvalFlatMap no longer cancels the others (#1437). A recover after the operation carries on where it ended the stream, and a take, headOption or forEach gets the results ahead of the failure. Without a recover, a failing parEvalMap run waits for its batch's slowest task.
  • Code that pulls a parallel operation by hand gets a Step.Fail for a failed task, where the pull threw (#1437). So does a task that returns null, and a parEvalFlatMap function that throws or returns null. An interrupted task, or a parEvalMap or parEvalMapUnordered function that throws, still ends the stream.
  • A finaliser or bracket release on a stream whose mapTask task fails runs when the stream is closed, not at the failure (#1029). A recover after them reads on first. Without one, a terminal operation closes the stream after any onError or mapError has seen the failure. Code that pulls by hand closes the tail of the Step.Fail.
  • Code that pulls a VStream by hand gets a Step.Fail for a failed mapTask task, where the pull threw (#1029). Its tail is the stream after the element, to close or read on from. An instanceof chain or default branch over Step misses it silently. Code compiled against 0.5.0 throws MatchException at an exhaustive switch until rebuilt.
  • A VStream hands over a checked exception as its task threw it (#1410). Code that called getCause() on a VTaskExecutionException from runSafe, or on a RuntimeException from parEvalMap, parEvalMapUnordered, merge or fromPublisher, now receives the exception itself, as it does from a finaliser. run() still wraps one.
  • A forked task's Throwable that is neither an Exception nor an Error is no longer wrapped (#1418). Code that called getCause() on the RuntimeException from Par, Scope.join, parEvalMap or parEvalMapUnordered now receives the Throwable itself. So does a caller of asCallable().call(), where a catch (Exception e) no longer catches it.
  • A VTaskPath handler sees a checked exception as its task threw it (#1396). Code that called getCause() on a VTaskExecutionException now receives the exception itself. That holds in runSafe and catching, in a handler or retry after via, in the future from runAsync(), and in VTaskContext.runOrElseGet and StreamTraversal.modifyVTask. unsafeRun() still wraps one.
  • recover, recoverWith, mapError and toEitherPath on a CompletableFuturePath keep a CompletionException that has no cause (#1398). A handler that was given an empty RuntimeException now gets the CompletionException.
  • A timed-out task is interrupted, and the timeout waits for it to stop (#1372). A task blocked in CompletableFuture.join(), or otherwise out of an interrupt's reach, holds the failure back until it finishes, and an interrupt of the caller now waits for it too. To fail at the time limit, as before, time out task.detachOnCancel().
  • A half-open CircuitBreaker rejects a call once successThreshold probes are running or have succeeded (#1481). It let every call through. The rejection is a CircuitOpenException whose status() is HALF_OPEN and retryAfter() the call timeout, and metrics().rejectedCalls() counts it. A fallback or onOpen mapping handles it; code that tests for OPEN should accept HALF_OPEN too.
  • A CircuitBreaker call still running when the circuit changes state no longer moves it (#1481), including across reset() and tripOpen(). Its outcome counted towards whatever state the circuit was in when it finished. The metrics still count it.
  • A half-open probe that an interrupt cannot reach holds its probe place until it stops (#1481). With the default threshold of one, every other call is turned away meanwhile. Protect task.detachOnCancel(), as for any call that can hang, so that the call timeout frees its place.
  • A CircuitBreaker whose recordFailure predicate throws counts the call as a failure (#1481). The predicate's exception replaced the call's. Now the call's own exception reaches the caller, with the predicate's attached as suppressed.
  • A call that one CircuitBreaker guards twice counts each attempt once (#1482). Its metrics() counted each outcome at both guards, so a circuit opened at half its failureThreshold. A fallback between the guards also kept the circuit closed. Now the circuit opens, and the outer guard turns calls away before the fallback runs, so put the fallback outside it.
  • A fromPublisher stream subscribes when a run's first pull runs, not when it is built (#1442). A hot publisher, such as a SubmissionPublisher, sends a run only what it publishes after the run subscribes, so what it publishes between building the stream and reading it is lost. Publish once the run has subscribed, which getNumberOfSubscribers() shows.
  • Code compiled against 0.5.0 that calls GenericPath.mapK or a VStreamTransformations method must be recompiled (#1455). Each now names Natural in its signature, so the old call fails with NoSuchMethodError. A mapK call that passes a NaturalTransformation still compiles, with a removal warning.
  • A VStream's recover, recoverWith, mapError and onError let an interrupt through (#1428). Their functions saw it, so recover ended the stream with its value, mapError replaced it and an onError hook logged it. Now the run fails with the InterruptedException. To give a value when a run is stopped, recover outside what stops it, as in toList().timeout(d).recover(e -> List.of()).
  • A VStream terminal operation on an interrupted thread fails before it reads (#1428). It read to the end unless a pull blocked. run() and runSafe() leave the status set after an interrupt, so a thread that goes on to read another stream fails at once. On a thread meant to carry on, clear the status first with Thread.interrupted().
  • VStreamPar.merge fails with the InterruptedException when its reader is interrupted (#1428). It threw a RuntimeException with the message "Merge interrupted". Now runSafe() gives the InterruptedException, run() throws it wrapped in a VTaskExecutionException, and a recover after the merge lets it through. Where code matched the message, match the InterruptedException instead.
  • prepend leaves the stream after its element alone until it is read (#1428). headOption() or take(1) after prepend closed that stream, running its onFinalize finaliser though nothing had read it, where VStream.of(x).concat(stream) did not. To run a finaliser however far a run reads, attach it after prepend. See onFinalize.
  • Closing a VStream you pulled by hand no longer closes that run (#1461). On a bracket, merge, fromPublisher, parEvalMapUnordered or onFinalize stream, close() after a pull ended the latest run, which could be another thread's. Close the tail the pull gave you instead. To stop another thread's run, time it out or interrupt that thread. See Pulling by hand.
  • getOrElseGet on a Path, and orElseGet on a Maybe, throw when the supplier returns null (#1489). They handed the null back. Code that relied on that gets NullPointerException, with supplier must not return null, or otherSupplier on a Maybe; return a value from the supplier.
  • cancel(true) on a future from runAsync() interrupts the task (#1412). A task that ran on after its future was cancelled is now interrupted: a sleep, a wait or a blocking socket call in it fails, and Java closes that socket. A task that catches the interrupt and carries on still runs on. The future from EffectBoundary.runAsync does the same.
  • Cancelling a CompletableFuturePath over a runAsync() future interrupts the task (#1412). That holds for Path.future(task.runAsync()), and for a path withTimeout or completeOnTimeout makes from it, with either flag. A map or via makes a new future, which breaks the chain. See CompletableFuture.

Spring

  • A VTaskPath request that times out interrupts its task (#1412). The 504 goes out as before. The task used to run on in the background, and is now interrupted, as Effect Paths says.
  • Start work that must outlive a VTaskPath response with its own runAsync() (#1412). Started inside the VTaskPath the controller returns, it keeps running when the timeout interrupts the request's task. Or raise hkj.virtual-threads.default-timeout-ms.
  • A timed-out CompletableFuturePath request over runAsync() interrupts its task (#1412). When hkj.async.default-timeout-ms runs out, the starter cancels the path's future, which now reaches the task.
  • ObservableEffectBoundary.runAsync passes a cancel through to the program, and records it (#1412). Cancelling its future, with either flag, interrupts the program and counts the invocation once, as an error, CancellationException. A cancel used to leave the program running and record nothing.

What stops a build that compiled

AreaNow fails the buildDo thisIssue
Effect PathsA chain step of another kind of Path, or with an error, environment, state, log or witness type the chain does not carryConvert the step, map its error, or type the chain on the wider error where it starts, as Common Compiler Errors shows#1314
Effect PathsCode that names a capability type with its old arguments, such as Effectful<A>, or a PathProvider returning Chainable<A>Write Effectful<?, A>, or <P, A> Effectful<P, A> in a helper that chains. The AddPathFamily recipe writes the first#1314
Effect PathsA chain whose receiver is typed with a wildcard error, such as EitherPath<? extends AppError, String>Widen it first, as in p.<AppError>mapError(e -> e).via(...), or declare it as EitherPath<AppError, String>#1314
Effect PathsA pattern or cast from a capability type to a concrete Path, such as case MaybePath<String> m on a Chainable<?, String>; a cast is unchecked, so it fails under -WerrorMatch from Composable<String>, or build the concrete Path directly#1314
Effect PathsA step after mapError on a MaybePath, TryPath or CompletableFuturePath that returns the Path it started fromReturn an EitherPath from the step (a VResultPath after a CompletableFuturePath), or use recover or mapException before changing the error type#1332
OpticsAn optic interpreter or other code of your own that names an OpticOp record: a pattern or type such as OpticOp.Get<?, ?> or OpticOp.Set<S, A>, or a constructor call such as new OpticOp.Get<>(source, getter)Add the third type argument, as in OpticOp.Get<?, ?, ?>, and pass a continuation, Function.identity(), to a constructor. Hand each result to op.k() and return what it gives, in place of Free.pure(result)#1154
Effect PathsAn exhaustive switch over VStream.Step, in code that pulls a stream by hand or in a stream of your ownAdd a Step.Fail case. A stream of your own passes it on with itself over the tail, as its Emit case does; code that stops there closes the tail and throws the error#1029
OpticsA call to the traversal @GenerateTraversals generated for a sorted container with a wildcard where it is ordered, such as PSortedSet<? extends String> or PSortedMap<? extends String, V>. The method is no longer generated, and a note says whyDeclare the component with concrete type arguments, such as PSortedSet<String>#1430
Effect PathsCode that keeps a VStreamTransformations result as a NaturalTransformation, such as NaturalTransformation<ListKind.Witness, VStreamKind.Witness> nt = VStreamTransformations.listToVStream()Declare it Natural, as the MigrateDeprecationsTo0_7_0 recipe does#1455
MappingA same-typed component declared @Nullable, copied by build into a record component, setter or builder setter declared non-null by an annotation or a @NullMarked scope, or by a plain merge into such a target componentMark the site @Nullable, or declare the source component non-null#1431
Build and toolingBuild logic that reads the hkjInstallSkills task's outputDir as a PathRead outputDir.get().asFile.toPath(), and expect .claude/skills at the root of the build in every project#1374, #1373

Deprecated for removal in 0.7.0

DeprecatedReplacementRecipe
VStream.closeAfterFailure(stream, failure)stream.close(); a failed mapTask task gives a Step.Fail, whose tail you close as an Emit'sBy hand
VStreamPar.parCollect(stream, batchSize)stream.toList()ReplaceParCollectWithToListRecipe, in MigrateDeprecationsTo0_7_0
VStreamPath.parCollect(batchSize)toList() on the pathReplaceParCollectWithToListRecipe
PathOps.parCollectVStream(path, batchSize)path.toList()ReplaceParCollectWithToListRecipe
Try.foldFailureFirst and TryPath.foldFailureFirstfold(failureMapper, successMapper), the same two functions in the same orderRenameTryFoldFailureFirst
Operation.FOLD_FAILURE_FIRSTOperation.FOLD, which Try.fold reportsBy hand
NaturalTransformation<F, G>, in org.higherkindedj.hkt.effectNatural<F, G>, which it extendsReplaceNaturalTransformationWithNatural, in MigrateDeprecationsTo0_7_0
FreePath.foldMapWith(interpreter, monad)foldMap(interpreter, monad)ReplaceNaturalTransformationWithNatural
FreeApPath.foldMapWith(interpreter, applicative)foldMapKind(interpreter, applicative)ReplaceNaturalTransformationWithNatural

Previous: Upgrading Next: v0.5.0