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.
- 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.
@ImportOpticspairs a JavaBeans-spelt wither with its getter: awitheMailbesidegeteMail()gives aneMail()lens.- A
@GenerateFocusmessage that offers a navigator asks first for the renames it needs. Following its fix line no longer leads into another note or error. - A
@GenerateMappingbean refusal's fix line compiles once followed, and names the near miss it passed over, such assetx(int)besideInteger 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
eMailsisseteMails, notsetEMails. A rename to a keyword such asdefaultis offered no marker, and a marker replaces the spec's own rename. A near miss such assetx(int)besideInteger 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 declareddeprecateddrew warnings inside the generated Impl, where your own@SuppressWarningscannot reach. A@GenerateMappingor@GenerateMergeImpl that names or calls a deprecated element now suppressesdeprecation, orremoval, 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 oftimeoutis offered no@Unmappedmarker, since the rename takes that name. A primitive word such assetx(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-typedMapcomponent's copy, andValidatedPrism'sparseValuesandbuildValues, rebuilt it as aLinkedHashMap, merging keys that were equal but not the same object. AnIdentityHashMapnow 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,
parseguards and leaf cache took or held anullwithout declaring it, so NullAway, run over a@NullMarkedpackage, failed inside the Impl. Each now declares its nulls. See Same-typed containers cross as copies. - The processor catches a
@Nullablecomponent thatbuildwould copy into one declared non-null (#1431).buildwrote the domain'snullinto 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@Nullablecomponent is not copied into a non-null one.
Optics
- Optics over a deprecated type compile under
-Werror(#1375). A record with aHashBagcomponent, 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@SuppressWarningscannot reach. Each generated member that names a deprecated class now suppressesdeprecation, orremoval, itself. See Build-time impact. - Traversals over commons-collections4's
HashMultiSet(#1379). Apache's commons-collections4 4.6.0 deprecatesHashBagand recommendsHashMultiSetin its place. AHashMultiSetcomponent now gets a traversal, as aHashBagone does, where it drew the note for a container no generator supports. See Apache Commons Collections. @ImportOpticspairs a JavaBeans-spelt wither with its getter (#1148). AwitheMail(String)besidegeteMail(), as Lombok'sbeanspeccapitalisation writes them, gives aneMail()lens, aswithEMailbesidegetEMail()does. The processor says why it refuses a class whose only wither iswith2D(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 andCharSequence name()from another is now read as returningString, so aLens<Src, String>over it builds. A@ViaBuilderchain reads an inheritedtoBuilder()orbuild()the same way. See Compiler Messages. - Messages that offer a navigator ask first for the renames it needs (#1272). The note to widen
itemsbeside anItemswith a navigator, and five other messages, now ask for one of the two to be renamed first. They also name a field such aslastthat the navigator could not reach. See Two fields with one navigator name. - An unknown
@GenerateFocusfield-list name is offered only a field the list can use (#1283). It no longer offers a name the list already holds.includeFieldsis offered a field that can have a navigator,widenFieldsone 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.mapapplies its function (#1154). It returned the instruction unchanged, so code that mapped a liftedOpticOpfailed later withClassCastException. EachOpticOprecord now carries a continuationk, as an@EffectAlgebraoperation does, and an interpreter hands each result toop.k()in place of wrapping it inFree.pure. See What an interpreter actually is.- Generated optics over a sorted collection keep its comparator (#1430). Traversals and Focus paths over
PSortedSet,PSortedMapand the Eclipse sorted sets rebuild with the source's comparator, so a custom order is kept. The…Rebuildingfactories onTraversalsandEachInstancesdo the same for your own optics, andgetOrderedTypeArgumentIndex()for your own generators. See Sorted Variants and Comparators. - An
IdentityHashMapwritten through an optic keeps every entry (#1458). Each optic that writes a map rebuilt it as aLinkedHashMap, so two keys that were equal but not the same object became one, and an entry was lost. AnIdentityHashMapnow comes back one, with every key. See TheforMapValues()Traversal. StreamTraversal.toTraversal()works over aVStream(#1442). OnforVStream(), or a composition built on it, the result read its source twice, so afromIteratorsource came out empty, and threwNoSuchElementExceptionwhen read again. It now reads the source once, and gives a stream that replays the results. See Converting to a Traversal.AffinePath.ofNullabletakes a getter that never returns null (#1488). Under a nullness checker it took only aFunction<S, @Nullable A>, so aFunction<S, String>variable was refused. It now takesFunction<? super S, ? extends @Nullable A>.
Effect Paths
- javac checks each chain step's kind of Path and error type (#1314). A step passed to
viaor 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. EitherPathgainsensure(#1338):ensure(test, onFail)keeps a success whose value passestestand turns any other success into a failure built from its value, so a guard no longer needs anifreturning aLeft. See EitherPath.TrampolinePathandFreePathcombine three paths withzipWith3(#1302): both already combined two paths withzipWith. The three-way combine stays lazy untilrun()orfoldMap, so a deep chain stays stack-safe. See Combinable (Applicative).NonDetPathgains six list operations thatListPathalready had (#1337):lastOption,anyMatch,allMatch,takeWhile,dropWhileandget. Each behaves as itsListPathcounterpart.NonDetPathgainsfoldRight,foldandfoldMap(#1322): withfoldLeft, it now has the four folds thatListPathandStreamPathoffer, and each behaves as itsListPathcounterpart. See the Effect Path Cheatsheet.StreamPathandNonDetPathgaintoListPath()(#1152):ListPath,StreamPathandNonDetPathnow each convert to the other two.Monoid.ofbuilds a monoid from its empty value and its operation, andMonoids.bigDecimalAddition()sums money exactly (#1356).Monoid.of(Duration.ZERO, Duration::plus)takes the place of an anonymous class, and an existingSemigroupcan be the operation. See Semigroup and Monoid.Maybe.fromEitherconverts anEitherto aMaybe(#1405): aRightbecomesJustwith its value, while aLeftbecomesNothing, discarding the left value.EithergainsfromOptional(#1406): converts a present JavaOptionaltoRightand an empty one toLeft, accepting either a direct error value or a supplier for the error.EithergainsorElseandorElseGet(#1407).orElse(other)returns aRight's value orother, andorElseGet(recovery)builds it from the error. A null argument is refused on aRightand on aLeft, and a recovery that returns null is refused on aLeft. A path usesgetOrElse, becauseorElsethere tries another path. See Reading an Either.EithergainstoOptionalandswap(#1404).toOptional()keeps aRight's value, and gives an emptyOptionalfor aLeft.swap()turns aLeftinto aRightand back, asEitherPath.swap()does.- A
foldcan returnnullto a caller whose result type is nullable (#1433).foldonEither,Validated,EitherOrBoth,EitherF,Try,EitherPath,ValidationPath,EitherOrBothPathandTryPath, and the deprecatedfoldFailureFirstonTryandTryPath, take a@Nullableresult type, so a nullness checker acceptsparsed.<@Nullable Integer>fold(_ -> null, n -> n). See Values, never null. VResultPathruns in one call withunsafeRun()andrunAsync(), asVTaskPathdoes (#803).unsafeRun()returns theEitherthatrun().run()gave, andrunAsync()starts the task on a virtual thread and returns a future of it. A typed failure still arrives as aLeft, and only an exception the task throws escapes.TryandTryPathgainfold, failure first (#546), asEither.fold,Validated.foldand their Paths'foldalready are.foldFailureFirst, which takes the same two functions in the same order, is deprecated for removal in 0.7.0, and theMigrateDeprecationsTo0_7_0recipe renames each call. ATrytraversal's generated code callsfold. See Reading a Try and Deprecated for removal in 0.7.0.ListPathandNonDetPathname the null they refuse (#1438). A null element, or amap,zipWithorzipWith3function that returns null, threw aNullPointerExceptionwith no message. The message now says which, asStreamPath's does.- A
Selectiveof your own runs only the branch its condition picks (#1358). The defaultwhenSran its effect for a false condition, and the defaultifSran both branches. Both now leave the choice toselect: one that keeps its contract skips what the condition does not pick, and one built fromapstill runs every effect. See Selective. mapErroronMaybePath,TryPathandCompletableFuturePathcarries the mapped error (#1332):recoverafter it sees that error, where it used to see the old one. A success is aRightand the mapper is not called.MaybePathandTryPathreturn anEitherPath.CompletableFuturePathreturns aVResultPath. See Recoverable (MonadError).toEitherPath,toMaybePathandtoTryPathfollowrecoveron a failedCompletableFuturePath(#1354). A cancelled future givesLeftof itsCancellationException, orNothing, instead of throwing.toTryPathholds the exception the future failed with, not aCompletionExceptionaround it.- A half-open
CircuitBreakeradmits a call only while running and successful probes are fewer thansuccessThreshold(#1481); it admitted every call. Any other call gets aCircuitOpenException, 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
CircuitBreakerguards 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'srecoverWith,mapErrorandonErroract on a failure at any element (#1029). They acted on the first pull only, and so didVStreamPath's. A laterrecoverstill resumes from the element after the failure. See VStream. - A
VStream'srecoverafter a failedmapTaskcarries on through the operators between them (#1029). The elements after the failure skipped those operators, so afilterlet them through, and amaporzipWithhanded on elements of the wrong type. A failed element is now aStep.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). Arecovercould 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,mapChunkedand theparEvalMapoperators hand on what they read before a failure (#1029). They dropped it, so aforEachafter them missed elements that aforEachwithout them saw. An interrupt still ends the stream at once.- A
recoverafterparEvalMap,parEvalMapUnorderedorparEvalFlatMapcarries on past a failed task (#1437). The task's failure ended the stream, so swappingmapTaskforparEvalMaplost recovery. Its element is now a failure in its place, as aftermapTask, and the other tasks run on. See When a task fails. parEvalFlatMapcloses the streams it built but never reached (#1454). When atakeorheadOptionstopped 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.closeAfterFailureis 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 thatstream.close()misses. See Deprecated for removal in 0.7.0.- A
VStreamfails with the exception its task threw (#1410). A checked one, such as anIOExceptionfrommapTask, a finaliser or a publisher, reached every terminal operation inside aVTaskExecutionException, or aRuntimeExceptionfromparEvalMap,parEvalMapUnorderedandmerge.run(), and the conversions that call it such astoListPath(), still wrap it. Par,ScopeandVStreamParfail with the task's ownThrowable(#1418). AThrowablethat is neither anExceptionnor anErrorreached the caller inside aRuntimeExceptiononce the task was forked, though the task run on its own failed with it.VTask.asCallable()now throws it unchanged.Instances.alternative(vstream())givesVStream'sAlternative(#1425). It failed with aClassCastException, because thevstream()token held a plain monad, so code had to nameVStreamAlternative.INSTANCE.Instances.monad(vstream())now returns that same object, which behaves as the monad did.VStreamPar.parEvalMapUnorderedemits 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 thanconcurrencytogether, so a slow consumer slows the reading.VStreamPath's does the same.VStreamPar.parCollectis deprecated for removal in 0.7.0 (#1427), withVStreamPath.parCollectandPathOps.parCollectVStream. A stream is read one pull at a time, so they run nothing in parallel and give the listtoList()gives. Put work you want run in parallel inparEvalMap's function, then calltoList(). TheMigrateDeprecationsTo0_7_0recipe rewrites each call. See Deprecated for removal in 0.7.0.- A
VTaskPathhands every handler the exception its task threw (#1396). A checked one, such as anIOException, reachedrunSafe,catchingandtoTryPath, and any step aftervia,thenorguarantee, wrapped in aVTaskExecutionException, so a retry onIOExceptiongave up at once. OnlyunsafeRun(), which cannot throw one, still wraps it. See VTaskPath. Context.recoverWithrecovers from a failure in an earliermaporflatMapfunction (#1443). An exception thrown by the function, or a function that returnednull, escaped arecoverWithlater in the chain, thoughrecoverandmapErrorcaught it. A recovery function that fails now hands its failure to the nextrecoverWithout.- A timeout cancels the task it timed out, as a
Scope's timeout does (#1372). When the time runs out,VTask.timeoutinterrupts the task and fails once it has stopped, so a retry over a timeout leaves no attempt running. EverywithTimeoutbutCompletableFuturePath's builds on it, as doResilienceBuilderand the circuit breaker'scallTimeout. 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 onVTask,IOPath,VTaskPath,VResultPathandVTaskContext. See Timeouts.- A
fromPublisherstream 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
Statechain built withmaporapno longer overflows the stack (#1448). Eachmapandapstep called the step before it, asWithStatePath'speek,zipWith,zipWith3andthendid, so tens of thousands of them threwStackOverflowError. They now run in the loopflatMapalready used. See State. Naturalis the library's one natural-transformation type (#1455).GenericPath.mapKtakes aNatural, andVStreamTransformationsgivesNaturals. The effect package'sNaturalTransformationnow extendsNatural, and is deprecated for removal in 0.7.0 withFreePath.foldMapWithandFreeApPath.foldMapWith. TheMigrateDeprecationsTo0_7_0recipe rewrites each use. See Deprecated for removal in 0.7.0.- A
VStreamterminal operation stops at an interrupt (#1428).toList,fold,drainand the others did not check between pulls, so neither a timeout nor a cancelledScopecould stop a stream whose pulls never block. Each now fails with anInterruptedExceptionand closes what it has read, so the finalisers there run. See Running a stream. - Stopping one run of a shared
VStreamleaves its other runs alone (#1461). Atake(0), a failed first pull or aclose()on a stream another thread was reading released that thread'sbracketresource, ran itsonFinalizefinaliser, or stopped itsmerge,fromPublisherorparEvalMapUnordered. Each run now closes only what it opened. Seebracketopens and closes. Free.foldMapkeeps what each step's value holds besides its result (#1460). For a target that runs each step at once, such asWriterorList, it kept only the result. AWriterorWriterT<Id, ...>lost its log, aListits other elements and anEitherOrBothits warnings. Each value now reaches the target whole. See Free Monad.Free.foldMapruns a deep program intoId,EitherorWriterwithout overflowing the stack (#1460). A recursive program of under a thousand steps overflowed it. IntoIOorState, a deep program already ran. AListstep that gives several results still adds to the stack, and so does each step of aReaderwhen it runs.- A
Freeprogram over instructions with noFunctorof their own can now be run, throughCoyoneda(#1470).Coyoneda.foldMap(natural, functor)runs a held value through an interpreter, andCoyoneda.interpreter(natural, functor)gives the interpreter that aFreePathneeds when itsFunctorisCoyonedaFunctor. See Coyoneda. - A long chain of
mapcalls on aCoyonedano longer overflows the stack (#1470). Eachmapwrapped the function before it, so lowering ten thousand of them threwStackOverflowError. The functions are now applied one after another in a loop. See Coyoneda. - A long
FreeApprogram no longer overflows the stack, and its interpreter is handed the instructions in the order they were written (#1470). A fold of ten thousandmap2orzipWithcalls threwStackOverflowErrorfromfoldMap, and so fromFreeApAnalyzer.analyseWith, which runs through it. The interpreter was also handed the instructions in reverse order. See Free Applicative. - Every Path's
getOrElseGetrefuses a supplier that returns null (#1489). OnMaybePath,EitherPath,ValidationPath,EitherOrBothPathandOptionalPath, and inMaybe.orElseGet, a null from the supplier was handed back, though the result type is non-null. It now throwsNullPointerException, asTryPath.getOrElseGetandEither.orElseGetalready did. OptionalContext.ioandJavaOptionalContext.iotake a supplier that never returns null (#1488). Under a nullness checker they took only aSupplier<@Nullable A>, so aSupplier<String>variable was refused. They now takeSupplier<? extends @Nullable A>.- Stop a
runAsync()task by cancelling its future (#1412).cancel(true)on the future fromrunAsync()onVTask,VTaskPath,VResultPath,VTaskContextorVStream, or fromEffectBoundary.runAsync, interrupts the task, as a future from anExecutorServicedoes, andcancel(false)leaves it running. See Running a task.
Spring
- A
VTaskPathresponse timeout interrupts its task (#1412). Whenhkj.virtual-threads.default-timeout-msruns 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-mismatchsays where to type the chain, as inwrite .<OrderError>mapError(...), or that the step's error needs mapping.path-type-mismatchnow reads method-reference steps,VResultPathand 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=warnfailed the build withplug-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,
hkjInstallSkillsandhkjDiagnosticsfailed, and so did every build withskills = true, which runshkjInstallSkills. 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-openrewriterelease takes a 0.4.x project'sTry.foldcalls to 0.6.0 (#546). The swap inMigrateDeprecationsTo0_5_0now leaves 0.6.0'sfoldalone, since it swaps only afolddeclared 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.AddPathFamilyrewritesChainable<A>toChainable<?, A>, and likewise the other capability types, aPathProvider'screatePathincluded. See 0.6.0 Path family. javadocruns in a build with the HKJ Gradle plugin applied (#1385). With preview features on, the default, javadoc received--enable-previewwithout the release it needs, sojavadocfailed, and so didbuildin a project that publishes a javadoc jar. The plugin now passes--releaseas well, unless the task passes its own-sourceor--release. See Defaults.- The benchmark options the docs describe now work (#1439). The benchmarks read only
-Pincludes, so-Pjmh.iterations,-Pjmh.fork,-Pjmh.profilers=gcand the rest did nothing, and the--includesmost pages gave failed. The build now reads each one, and Benchmarks lists them. - A generator plugin needs only
hkj-processorfrom HKJ (#1376).BaseTraversableGeneratormoves 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 anhkj-processor-pluginsline it added to reach the base class. Itsmodule-info.javakeepsrequires 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
DetectPossibleNullSuccessValuesRecipemarks a@Nullablevalue passed on to a success factory, and a@Nullablemethod read byTry.ofor amap. It marks a null check in a later step, which 0.5.0 never hands anull. A 0.5.0 project can run it alone. See 0.5.0 deprecation migration. MigrateDeprecationsTo0_5_0puts a lambda's mark on thenullit returns (#1370). The mark sat at the start of the call's chain, which could be many lines before thenull. See 0.5.0 deprecation migration.
Documentation
StreamPathdocs 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, andtoStreamPathsays the non-null focuses are collected into a list when it is called.- The StreamPath docs say when the source is read (#1278).
Path.streamreads its whole stream when it is called, so the stream must be finite.Path.streamIterate,StreamPath.generateandStreamPath.fromSupplierbuild a fresh stream each time the path runs, sotakecan limit an infinite one. See Choosing the Right Streaming Tool. - The Effect Paths glossary gains three entries (#1301):
FreeApPath,FreePathandGenericPath. Each gives a definition, its factories onPath, and its main operations. See Effect Paths. - The Effect Paths glossary gains four more entries (#1258):
ReaderPath,TrampolinePath,WithStatePathandWriterPath. Each gives a definition, its factories onPath, 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,StreamPathandVStreamPath. Each gives a definition, its factories onPath, 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
IOPathstep in anEitherPathchain, which throws when it runs; it now builds anIOPathchain. The Quickstart, a migration recipe and thehkj-guideandhkj-springskills 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}andLifts the {@code Charge} operation. A path or optic type keeps the article its name takes, as inCreates an {@link AffinePath}. - The
hkj-guideskill turns guard clauses intoensure(#1338). A new migration recipe replaces a run ofif (!ok) return Either.left(...)checks with oneensurestep for each, and the skill's Error Handling table listsensure. - The
Profunctorjavadoc 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, andBifunctor's javadoc namesFunctionas the onlyProfunctorinstance. See Profunctor. - The
VTaskjavadoc says which thread runs a task (#1396).run()runs it on the calling thread, andrunAsync(),ParandScoperun 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.oforTry.of(#1371). In JSpecify mode alone, NullAway never checks what a lambda returns when passed as a JDKSupplier,CallableorBiFunction, as toVTask.of,Try.oforIO.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_0while still on 0.4.x (#546).foldreturns onTryandTryPath, failure first. A success-firstfoldleft from 0.4.x compiles again where both functions accept either argument, and runs them swapped:t.fold(v -> true, e -> false)now givestruefor 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
equalscannot tell apart, unless it is anIdentityHashMap(#1483). A same-typedMapcopy,parseValuesandbuildValueslost an entry; they now throw. Hold such a map as anIdentityHashMap, 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, andmap2tomap5on its monad, throwNullPointerExceptionwhen their function returns null (#1318). They threwKindUnwrapException, so catching that, orIllegalArgumentException, around the call no longer catches it. See Values, never null.FunctionKindHelper.narrowthrowsKindUnwrapException, as the other kind helpers do (#1318). A nullKindthrewNullPointerException, so catching that no longer catches it. Another type'sKindthrewIllegalArgumentException, whichKindUnwrapExceptionextends, so catching that still does. See Whennarrowthrows.- A
Selectiveof your own no longer runs the effects its condition skips (#1358), unless itsselectis built fromap. The defaultwhenSran its effect on a false condition, and the defaultifSran 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.recoverWithnow catches a failure from an earliermaporflatMapfunction (#1443). Code that let such a failure escape arecoverWithgets the recovered value instead, and a recovery function's own failure reaches the nextrecoverWithout. A checked exception such a function throws, as Kotlin code can, reachesrun()inside aContextException, as one fromfaildoes.Free.foldMapinto aWriter,ListorEitherOrBothgives what each step's value holds (#1460). AWriter's log is no longer empty, and anEitherOrBoththat warned givesBoth, where it gaveRight. AListtarget runs the rest of the program, and its interpreter, once for each element a step gives.Constand the optics' folds refuse a null value (#1470).new Const<>(null)throwsNullPointerException, as does amapFirstorbimapfunction 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 aMonoid<Unit>, notMonoid<Void>. See Values, never null.- A
FreeApinterpreter 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 intoIdthat logs or calls a service, now does so in that order. Results, and the failuresValidatedaccumulates, were already in order.
Optics
- Of two withers for one getter,
@ImportOpticssets through the capitalised spelling (#1148): the lens for a class with bothwithEMailandwitheMailbesideeMail()set through whichever the class declared first. It now sets throughwithEMail, and a note nameswitheMail, which a spec interface's@Withercan choose. - An optic interpreter that wraps each result in
Free.pureskips the operation's continuation (#1154): a program fromOpticProgramsruns as before, but a function mapped onto anOpticOp, throughOpticOpFunctororFree.translate, is not applied. Hand the result toop.k()and return what it gives. - A sorted collection keeps its order through a generated optic (#1430). A traversal over a
PSortedSetorPSortedMap, 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
IdentityHashMapholding two keys thatequalscannot tell apart (#1458), such as aHashMapwhose key changed after it went in. Writing one lost an entry; it now throwsIllegalArgumentException. Copy such a map into anIdentityHashMapfirst. AnIdentityHashMapcomes 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
OpticOprecord throwsNullPointerException, and so does anOpticProgramsfactory 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.setandsetAllstill accept a null new value.
Effect Paths
- A
ValidationPathkeeps its own semigroup through a step (#1314): the result ofvia,then,recoverWithandorElseaccumulated with the step's semigroup, and now accumulates with the receiver's. AftermapError, whose placeholder semigroup throws, a later accumulation now throws even past a step that had a semigroup; usemapErrorWithinstead. - An
EitherOrBothPathkeeps its own semigroup through a recovery (#1314): the result ofrecoverWithandorElseaccumulated with the recovery's semigroup, and now accumulates with the receiver's, asviaandthenalready did. mapErroronMaybePath,TryPathandCompletableFuturePathapplies the mapper (#1332): it used to hand back the same path, and it never called the mapper. A failure now runs the mapper, andrecoversees that error.MaybePathandTryPathreturn anEitherPath.CompletableFuturePathreturns aVResultPath.mapExceptionstill stays aTryPath.toEitherPathandtoMaybePathon a cancelledCompletableFuturePathno longer throw (#1354). They giveLeftof theCancellationException, andNothing.toTryPathon a failed future holds that future's exception. Code which caught aCancellationExceptionaroundtoEitherPathortoMaybePath, or matched aCompletionExceptionin atoTryPathfailure, no longer sees one.- A
VStream'srecoverWith,mapErrorandonErrornow 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, andrecoverwhen it ends the stream, close what they stop reading. - A
VStream'srecoverafter a failedmapTaskpasses the elements after the failure through the operators between them (#1029). Amap,filterortakethere now applies to them. ArecoverafterVStreamPar.mergeof several streams ends the stream there, and one after a task whose exception has suppression disabled carries on. - A failed
mapTaskelement holds its place in aVStream(#1029).dropanddropWhiledrop it within the prefix they drop, so a failure there no longer fails the stream.zipWithpairs it with the other stream's element and ends with the shorter stream, andthrottleandmeteredpace it. chunk,chunkWhile,mapChunkedand theparEvalMapoperators hand on what they read before a failure, then fail (#1029). A batch function or task now runs for those elements, with or without arecover, so aforEachorheadOptionafter them sees them where it saw only the failure.- A failed task in
parEvalMap,parEvalMapUnorderedorparEvalFlatMapno longer cancels the others (#1437). Arecoverafter the operation carries on where it ended the stream, and atake,headOptionorforEachgets the results ahead of the failure. Without arecover, a failingparEvalMaprun waits for its batch's slowest task. - Code that pulls a parallel operation by hand gets a
Step.Failfor a failed task, where the pull threw (#1437). So does a task that returns null, and aparEvalFlatMapfunction that throws or returns null. An interrupted task, or aparEvalMaporparEvalMapUnorderedfunction that throws, still ends the stream. - A finaliser or
bracketrelease on a stream whosemapTasktask fails runs when the stream is closed, not at the failure (#1029). Arecoverafter them reads on first. Without one, a terminal operation closes the stream after anyonErrorormapErrorhas seen the failure. Code that pulls by hand closes the tail of theStep.Fail. - Code that pulls a
VStreamby hand gets aStep.Failfor a failedmapTasktask, where the pull threw (#1029). Itstailis the stream after the element, to close or read on from. Aninstanceofchain ordefaultbranch overStepmisses it silently. Code compiled against 0.5.0 throwsMatchExceptionat an exhaustive switch until rebuilt. - A
VStreamhands over a checked exception as its task threw it (#1410). Code that calledgetCause()on aVTaskExecutionExceptionfromrunSafe, or on aRuntimeExceptionfromparEvalMap,parEvalMapUnordered,mergeorfromPublisher, now receives the exception itself, as it does from a finaliser.run()still wraps one. - A forked task's
Throwablethat is neither anExceptionnor anErroris no longer wrapped (#1418). Code that calledgetCause()on theRuntimeExceptionfromPar,Scope.join,parEvalMaporparEvalMapUnorderednow receives theThrowableitself. So does a caller ofasCallable().call(), where acatch (Exception e)no longer catches it. - A
VTaskPathhandler sees a checked exception as its task threw it (#1396). Code that calledgetCause()on aVTaskExecutionExceptionnow receives the exception itself. That holds inrunSafeandcatching, in a handler or retry aftervia, in the future fromrunAsync(), and inVTaskContext.runOrElseGetandStreamTraversal.modifyVTask.unsafeRun()still wraps one. recover,recoverWith,mapErrorandtoEitherPathon aCompletableFuturePathkeep aCompletionExceptionthat has no cause (#1398). A handler that was given an emptyRuntimeExceptionnow gets theCompletionException.- 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 outtask.detachOnCancel(). - A half-open
CircuitBreakerrejects a call oncesuccessThresholdprobes are running or have succeeded (#1481). It let every call through. The rejection is aCircuitOpenExceptionwhosestatus()isHALF_OPENandretryAfter()the call timeout, andmetrics().rejectedCalls()counts it. A fallback oronOpenmapping handles it; code that tests forOPENshould acceptHALF_OPENtoo. - A
CircuitBreakercall still running when the circuit changes state no longer moves it (#1481), including acrossreset()andtripOpen(). 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
CircuitBreakerwhoserecordFailurepredicate 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
CircuitBreakerguards twice counts each attempt once (#1482). Itsmetrics()counted each outcome at both guards, so a circuit opened at half itsfailureThreshold. 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
fromPublisherstream subscribes when a run's first pull runs, not when it is built (#1442). A hot publisher, such as aSubmissionPublisher, 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, whichgetNumberOfSubscribers()shows. - Code compiled against 0.5.0 that calls
GenericPath.mapKor aVStreamTransformationsmethod must be recompiled (#1455). Each now namesNaturalin its signature, so the old call fails withNoSuchMethodError. AmapKcall that passes aNaturalTransformationstill compiles, with a removal warning. - A
VStream'srecover,recoverWith,mapErrorandonErrorlet an interrupt through (#1428). Their functions saw it, sorecoverended the stream with its value,mapErrorreplaced it and anonErrorhook logged it. Now the run fails with theInterruptedException. To give a value when a run is stopped, recover outside what stops it, as intoList().timeout(d).recover(e -> List.of()). - A
VStreamterminal operation on an interrupted thread fails before it reads (#1428). It read to the end unless a pull blocked.run()andrunSafe()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 withThread.interrupted(). VStreamPar.mergefails with theInterruptedExceptionwhen its reader is interrupted (#1428). It threw aRuntimeExceptionwith the message "Merge interrupted". NowrunSafe()gives theInterruptedException,run()throws it wrapped in aVTaskExecutionException, and arecoverafter the merge lets it through. Where code matched the message, match theInterruptedExceptioninstead.prependleaves the stream after its element alone until it is read (#1428).headOption()ortake(1)afterprependclosed that stream, running itsonFinalizefinaliser though nothing had read it, whereVStream.of(x).concat(stream)did not. To run a finaliser however far a run reads, attach it afterprepend. SeeonFinalize.- Closing a
VStreamyou pulled by hand no longer closes that run (#1461). On abracket,merge,fromPublisher,parEvalMapUnorderedoronFinalizestream,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. getOrElseGeton a Path, andorElseGeton aMaybe, throw when the supplier returns null (#1489). They handed the null back. Code that relied on that getsNullPointerException, withsupplier must not return null, orotherSupplieron aMaybe; return a value from the supplier.cancel(true)on a future fromrunAsync()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 fromEffectBoundary.runAsyncdoes the same.- Cancelling a
CompletableFuturePathover arunAsync()future interrupts the task (#1412). That holds forPath.future(task.runAsync()), and for a pathwithTimeoutorcompleteOnTimeoutmakes from it, with either flag. Amaporviamakes a new future, which breaks the chain. See CompletableFuture.
Spring
- A
VTaskPathrequest 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
VTaskPathresponse with its ownrunAsync()(#1412). Started inside theVTaskPaththe controller returns, it keeps running when the timeout interrupts the request's task. Or raisehkj.virtual-threads.default-timeout-ms. - A timed-out
CompletableFuturePathrequest overrunAsync()interrupts its task (#1412). Whenhkj.async.default-timeout-msruns out, the starter cancels the path's future, which now reaches the task. ObservableEffectBoundary.runAsyncpasses 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
| Area | Now fails the build | Do this | Issue |
|---|---|---|---|
| Effect Paths | A chain step of another kind of Path, or with an error, environment, state, log or witness type the chain does not carry | Convert the step, map its error, or type the chain on the wider error where it starts, as Common Compiler Errors shows | #1314 |
| Effect Paths | Code 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 Paths | A 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 Paths | A 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 -Werror | Match from Composable<String>, or build the concrete Path directly | #1314 |
| Effect Paths | A step after mapError on a MaybePath, TryPath or CompletableFuturePath that returns the Path it started from | Return an EitherPath from the step (a VResultPath after a CompletableFuturePath), or use recover or mapException before changing the error type | #1332 |
| Optics | An 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 Paths | An exhaustive switch over VStream.Step, in code that pulls a stream by hand or in a stream of your own | Add 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 |
| Optics | A 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 why | Declare the component with concrete type arguments, such as PSortedSet<String> | #1430 |
| Effect Paths | Code 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 |
| Mapping | A 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 component | Mark the site @Nullable, or declare the source component non-null | #1431 |
| Build and tooling | Build logic that reads the hkjInstallSkills task's outputDir as a Path | Read 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
| Deprecated | Replacement | Recipe |
|---|---|---|
VStream.closeAfterFailure(stream, failure) | stream.close(); a failed mapTask task gives a Step.Fail, whose tail you close as an Emit's | By hand |
VStreamPar.parCollect(stream, batchSize) | stream.toList() | ReplaceParCollectWithToListRecipe, in MigrateDeprecationsTo0_7_0 |
VStreamPath.parCollect(batchSize) | toList() on the path | ReplaceParCollectWithToListRecipe |
PathOps.parCollectVStream(path, batchSize) | path.toList() | ReplaceParCollectWithToListRecipe |
Try.foldFailureFirst and TryPath.foldFailureFirst | fold(failureMapper, successMapper), the same two functions in the same order | RenameTryFoldFailureFirst |
Operation.FOLD_FAILURE_FIRST | Operation.FOLD, which Try.fold reports | By hand |
NaturalTransformation<F, G>, in org.higherkindedj.hkt.effect | Natural<F, G>, which it extends | ReplaceNaturalTransformationWithNatural, in MigrateDeprecationsTo0_7_0 |
FreePath.foldMapWith(interpreter, monad) | foldMap(interpreter, monad) | ReplaceNaturalTransformationWithNatural |
FreeApPath.foldMapWith(interpreter, applicative) | foldMapKind(interpreter, applicative) | ReplaceNaturalTransformationWithNatural |