v0.5.0 (26 October 2026)

v0.5.0 on GitHub

This release makes the optics generated for a record easier to reach and safer to hold. Navigators are on by default, so OrderFocus.lines().quantity() chains through a list from a bare @GenerateFocus, and every optic and Focus path takes a @Nullable focus. The core types now refuse null as a success, as an error and as a lazy value, so a null fails where it is made rather than where it is read. Resources are released exactly once, whatever stops the work. Every API deprecated for removal in 0.5.0 is gone, and MigrateDeprecationsTo0_5_0 migrates most calls.

At a glance

  • Navigators are on by default, and a navigator is a path. UserFocus.address().street() and OrderFocus.lines().quantity() work with a bare @GenerateFocus, and the navigator goes wherever a path goes.
  • Optics over a nullable field pass a nullness checker. Every optic and Focus path type takes a @Nullable focus, and a bridge into an Effect Path can say what a null focus becomes.
  • No success, error or lazy value is null. Either, Try, IO, VTask, VStream and every Path over them refuse null as a success, an error or a lazy value, and Unit stands for a step with nothing to return.
  • Resources are released exactly once. A VStream, a Resource, a bracket and a Scope clean up whatever stops them: an early take, a failure, a timeout or a cancellation.
  • Optics cost less. A read through a traversal no longer rebuilds what it reads, a fold stops at its answer, and a Focus path rebuilds a 100,000-element list in about a millisecond.
  • Every API deprecated for removal in 0.5.0 is gone. MigrateDeprecationsTo0_5_0 migrates most calls, and the build table gives the fix for the rest. A Focus path's untyped shortcuts are deprecated for removal in 0.6.0, and MigrateDeprecationsTo0_6_0 replaces them.
  • The Optics chapter is rewritten. It reads in order from a first nested update to a PATCH, on one order service, with every example compiled and checked.
  • Some changes alter what a running program does. Many others stop the build, so read Upgrading from 0.4.11 first.

Mapping

Generic specs

  • A bounded generic spec covers only what its bound admits (#1118). A spec over Page<T> with T extends Number covered a Page<String> field and broke the generated code. That field now reports no usable source, naming the bound, or nests through a spec for Page<String> declared beside it. See A bounded spec covers only what its bounds admit.
  • A generic spec over an array of its parameter leaves a primitive array alone (#1091). No type argument is a primitive, so a spec over Page<T[]> no longer covers a Page<int[]> field, which crashed the processor or failed in generated code. The field reports no usable source instead. See The boundaries of a generic spec.
  • An element-mapped spec nests where a type argument passes through unconverted (#1122). A spec over TaggedPage<T, X> whose leaf converts only the elements broke the generated code wherever it nested, since no leaf names X. The generated call now spells out its type arguments, keeping a @Nullable the records declare. See How an element-mapped spec nests.
  • A merge fills a component through a generic spec (#1123). A threaded spec such as PageMapping<T> now fills a merge as it nests in a mapping. An element-mapped spec is not supported yet, and the merge says so, offering the leaf that builds it. See How a merge fills.
  • The most specific of several specs for a pair maps it (#1123). TagPageMapping beside PageMapping<T>, or PageMapping<T> beside CodecPageMapping<T, TDto>, made a field ambiguous. The more specific now maps it, and a compiler note names a dependency's spec it displaced. See The most specific spec for a pair is used.

Beans, builders and codecs

  • More Lombok @Singular collections map (#1110). A build-only collection whose name has a capital second letter, such as eMails, is written whole, whichever capitalisation Lombok gives its clear method. So is one of an unbounded wildcard, or a Guava ImmutableTable. An Iterable property without type arguments is no longer taken for one. See A Lombok @Singular collection.
  • A method that only starts like an accessor is never read as one (#1128). A name that runs on in lower case after its get, is or set prefix is a word, while geteMail() stays the JavaBeans getter of eMail. So getaway() no longer reads away, and a builder's settings(String) writes settings. See How a bean is read and written.
  • A word a component needs is refused, with the accessor to write (#1128). A bean's getx() and setx(int), with no accessor of x beside them, used to be read as x. The error now offers getX() and setX(int), or an @Unmapped marker. See A method that only starts like an accessor.
  • A builder with both name(...) and setName(...) maps (#1128): the two were taken for rival overloads, which javac never weighs against each other, and the property was refused. name(...) writes it. See How a bean is read and written.
  • The processor names what an accessor-style leaf or marker meant (#907): a leaf written getEmail() for the component email now draws Did you mean 'email()'?, as a near misspelling already did. An @Unmapped or @ReadOnly marker named after its accessor, setStamp() or getId(), draws the property the same way. See Compiler Messages.
  • A date codec refuses a formatter that cannot parse what it writes (#1144): StandardCodecs.localDate(formatter) and offsetDateTime(formatter) throw IllegalArgumentException when built over one, such as a pattern with no year. Such a codec used to reject every input while quoting a sample it rejected itself. See Canonical forms only.
  • A misspelt @MapKey value suggests the component's bare name (#1112). The hint now offers byOwner rather than byOwner(), so it can be used as the annotation's value. Hints that name methods keep their parentheses.

Optics

  • @GenerateFocus generates navigators by default, and a navigator is a path (#1033). UserFocus.address().street() works on a bare annotation, and generateNavigators = false turns navigators off. A navigator can be held as a FocusPath or passed to an Effect Path's focus, and every path method works on it. See Fluent navigation with generated navigators.
  • OrderFocus.lines().quantity() chains with no .via(...) (#1103). A List, Set, Collection, Optional or Maybe of records gets a navigator, and so does one nested in another, such as List<Optional<LineItem>>, or a Kind the processor traverses. The navigator is the path the method returned before, so code that declares that path type still compiles. See Which fields get a navigator.
  • OrderHistoryFocus.orders().customer().email() keeps navigating after a hop that widens the path (#1214). Each navigator nests a form for each wider path type, AffineForm and TraversalForm, and a widening hop into a record field returns it. A record that writes generateNavigators = true is told when a form is left out. See Which fields get a navigator.
  • A navigator steps into a container only where the record widens it (#1033). A Map, an array or a third-party collection of @GenerateFocus records gets a navigator under widenCollections = true, or when the new widenFields names the field. So turning navigators on never changes what a field's method focuses on. See The ZERO_OR_MORE Asymmetry.
  • A navigator reaches into a generic record (#739, #1061): a field such as Revision<EmailAddress> email gets a navigator instantiated with its type arguments, so ContactHistoryFocus.email().current() chains. Where a field gets no navigator, as one given a wildcard does, its note names the path its method returns. See A target with type parameters.
  • A field named like a path method is handled at the declaration (#1033). A navigator never reaches a field such as last, since name().last() would call FocusPath.last(). The field reaching such a record keeps its plain path, with a note, or is refused under generateNavigators = true. Fields named get, set and count navigate. See A field named like a path method.
  • Fields such as items and Items keep one navigator between them (#1259). Where both would have a navigator, their classes would share a name, so the first in declaration order keeps it. The later one keeps its plain path, with a note, or is refused where the record writes generateNavigators = true. See Two fields with one navigator name.
  • @GenerateFocus checks the names in includeFields, excludeFields and widenFields (#1104). A name that matches no component is an error at the value, offering the component meant, such as positions for "postions". A list the record never reads warns, as does a widenFields name whose field cannot widen. See Compiler Messages.
  • A @GenerateFocus setting that names a constant javac cannot resolve leaves only javac's error (#1104), such as includeFields = Names.MISSING. The processor stopped with an exception, which hid that error.
  • @GenerateFocus warns of an includeFields name whose field has no navigator (#1181, #1201, #1207). Naming only a String field, say, left every field without a navigator, and nothing said why. The warning says why and offers a fix: another field to name, a record to annotate, a container to widen, or includeFields to remove, with navigators off. See Compiler Messages.
  • @GenerateFocus reports a TraversableGenerator priority tie once (#1081): the warning lands on the component that reaches the tied type, however many navigators reach it. It used to repeat for each one. See Compiler Messages.

Optics and null

  • Optics over a nullable field pass a nullness checker (#767, #1017): every optic and Focus path type, and Kind, takes a nullable type, so the lens generated for a @Nullable component checks under JSpecify. A bridge into an effect type can say what a null focus becomes, as toEitherPath(source, error) does. See A focus that may be null.
  • nullable() keeps the focus type (#1017): FocusPath.nullable() and AffinePath.nullable() return the focus type without its null, so a chained focus(path.nullable(), error) or a var infers it where it inferred Object. A call written path.<String>nullable() still compiles. See .nullable(): Handle Null Values.
  • A Focus path passes over a null it would step into (#1062). A @Nullable List, Optional, Either or other container now widens through .nullable() first, so its path is empty when the container is null, where it threw NullPointerException. A navigator into Map<String, @Nullable Address> reads the other elements and leaves a null one in place. See Where a @Nullable counts.
  • modifyAllEither, modifyAllValidated and modifyAllMaybe keep a null element their traversal passes over (#1065): over filtered or a prism that leaves a null element alone, modifyAllValidated threw and modifyAllMaybe gave Nothing. Each now keeps the null, and still rebuilds nothing once a step fails. So do OpticOps' validating modifiers. See modifyAllEither: First Error Only.

The containers a Focus path widens

  • A Maybe component's Focus path reads and writes its Just value (#757): @GenerateFocus now widens a Maybe field, and one nested in another container such as List<Maybe<A>>, through .some(Affines.just()). It used the Optional-only .some(), so the first read or write threw ClassCastException. See Supported Container Types.
  • A raw or wildcard Maybe, Set or Collection component widens, as a List does (#1251). Set<? extends Widget> widens to a TraversalPath onto Widget. In 0.4.11 such a Set or Collection was rejected at the declaration, and such a Maybe threw ClassCastException on first use. See Supported Container Types.
  • @GenerateFocus widens an array, as the Focus pages say (#1062). Under widenCollections = true, a String[] or other array of a reference type is a TraversalPath over its elements, and an array of @GenerateFocus records gets a navigator over them. A primitive array stays a plain path. See Supported Container Types.
  • A Focus path through a custom container whose generator names no optic stops at the container (#1059), as if no generator supported it. It used to widen through the Optional-only .some() or the List-only .each(), and threw ClassCastException on first use for other containers. The generator still serves @GenerateTraversals and @ImportOptics. See Registering your own container type.
  • A Focus path's untyped shortcuts are deprecated for removal in 0.6.0 (#1105). each(), some(), at(index) and the others compile on any focus, then throw ClassCastException on the wrong one. Each typed form, such as each(EachInstances.listEach()), reads the element type from the path and does not compile on another focus. See Deprecated for removal in 0.6.0.
  • EachIndexed is the one way to an indexed traversal from an Each (#1261): Each.eachWithIndex(), deprecated since 0.4.7, is removed. It let the caller choose the index type, so a wrong one compiled, then threw ClassCastException. indexedTraversal() fixes the index type in its signature. See Using Indexed Traversal.

Composition and cost

  • Any two of Iso, Lens, Prism, Affine and Traversal compose directly with andThen (#1021). An Iso followed by a Traversal now returns a Traversal, and so does a Traversal followed by an Affine or an Iso, with no asTraversal() first. The composition table is read from the overloads, so it always matches the library. See Composition Rules Table.
  • A read through a traversal no longer rebuilds what it reads (#1056). Traversals.getAll, a TraversalPath's reads, the indexed reads and IxedInstances.get read each focus as asFold() does, so reading a list or map through an optic no longer builds a new one. A prism or affine that matches builds nothing it throws away. See What reads cost.
  • A fold's query stops at its answer (#1056). preview, find, exists, all and isEmpty on Fold, TraversalPath and IndexedFold visit no focus after the one that settles them, so exists over a long list returns at its first match. A path from traced still reads every focus, so its observer sees them all. See What reads cost.
  • A Focus path steps into a List in time proportional to the list (#1063). Its .each(EachInstances.listEach()), which every generated List path uses, now rebuilds the list once instead of copying it at every element. On 100,000 elements that took several seconds, and now takes about a millisecond. EachInstances.arrayEach() rebuilds an array the same way. See Traversal allocation.
  • A map written through an optic keeps its source's order (#1022): every optic that writes a Map, from Traversals.forMapValues to AtInstances.mapAt and Setter.forMapValues, hands back a map in the source's iteration order. An updated key keeps its place and a new key goes at the end. See Persistent and Third-Party Maps.
  • modifyWhen and branch on a Traversal call only the function their predicate picks (#1026). Each element is now tested first, so a function may assume the condition that guards it, and an expensive one runs only where it is needed. Lens.modifyWhen, modifyBranch and setIf test first too. See Complete, Runnable Example.
  • Setter.forList and Setter.forMapValues give the same result on every run (#1018): each time the effect from modifyF runs, it builds its own list or map. So an IO gives an equal value on every run, and the List applicative gives every combination of choices. Traversals.traverseMapValues does the same for an empty map. See Sequencing Effects in Collections.

Copy strategies, Lombok and protobuf builders

  • @ViaBuilder rebuilds through Lombok's @SuperBuilder(toBuilder = true) (#1110). Its toBuilder() hands back a builder typed with wildcards, PetBuilder<?, ?>, whose setter and build() the processor now follows as javac does. So does a hand-written builder handed back with wildcards, where a step returns one of its own type parameters. See Non-Standard Naming.
  • More @ViaBuilder chains are checked at the spec (#1110). A spec over Node<?> whose toBuilder() returns a type parameter of Node builds. A builder bounded by more than one type is checked too, and a chain that cannot compile is refused at the spec rather than in the generated file. See the chain the lens rebuilds through.
  • A @ViaBuilder lens replaces a collection its builder would add to (#1110). For a Lombok @Singular List<String> tags, or a protobuf-java message's addAllTags or putAllPoints, the generated lens first calls the clearTags() or clearPoints() written beside it, so the lens laws hold. That covers @Builder and @SuperBuilder, with or without a lower-case setterPrefix. See Non-Standard Naming.
  • A @Wither that returns its class's own type parameter works on a source written with a wildcard (#1110): S withName(String) on a Shape<S extends Shape<S>> rebuilds a Shape<?>. See Copy Strategies.
  • A spec's diagnostics never offer a deprecated method (#1110). Following one failed a build under -Werror in the generated file. A lens named after a protobuf repeated or map field is offered getTagsList() or getPointsMap(), never the deprecated getPoints(). See the missing accessor.
  • The processor suggests methods spelt the way each copy strategy calls them (#907). A getter written name against a bean's getName() now draws Did you mean 'getName'?, and getActive draws 'isActive'. A setter or wither is offered in the convention its strategy needs: a builder's setName or name, a bean's setName, @Wither's withName. See Compiler Messages.
  • @ViaConstructor holds its order to the constructor (#906). An order that passes a value where the constructor's parameter names say another belongs, or that no constructor takes, is now refused at the spec method. Leave parameterOrder out and it is read from those names. See @ViaConstructor: constructor-only value types.
  • A @ViaConstructor order that leaves a value out says so (#1067). A written order that rebuilds through a shorter constructor now warns, naming the values longer constructors take. @SuppressWarnings("shorter-constructor") keeps quiet a constructor that works the value out on purpose. A variable-arity constructor is held to its parameter names too. See the warning's entry.
  • A @ViaCopyAndSet lens keeps what its withX setter returns (#1108). A withTitle setter had its new value dropped. The lens now hands back what it returns where every method of that name the new value can be passed to returns the source type. One it cannot keep warns, unless the lens method is annotated @SuppressWarnings("dropped-result"). See @ViaCopyAndSet.
  • @ImportOptics takes no word for a wither (#1128). A name that runs on in lower case after with is a word, so a withdraw(long) beside getDraw(), which gave a draw lens that added to draw rather than setting it, now gives none. A with2D(int) beside get2D() no longer stops the processor. See Wither Classes to Lenses.
  • A spec lens that would read or rebuild unchecked through a raw type is refused at the spec method (#905). A raw accessor read as List<String>, or a setter called through a raw builder, left an unchecked operation in generated code, which failed a build under -Xlint:unchecked -Werror. The error names the member at fault. See Compiler Errors.

Prisms and @InstanceOf

  • A prism onto an inner class of a generic class compiles under -Werror (#886): a Dot declared inside class Outlines<X> gets Prism<Outlines.Outline, Outlines<?>.Dot>, from @GeneratePrisms, @ImportOptics and an @InstanceOf spec method. A generic inner class gets a prism too, where @InstanceOf refused one. See Generating the Prisms and Parameterised Targets.
  • @InstanceOf reports a source argument outside the target's bound at the spec method (#1082). A Gauge<Y extends Number> reached from a Node<U> whose U could be anything used to fail in the generated file, where javac refused Gauge<U>. Where one bound would do, the message gives the declaration to write. See Parameterised Targets.
  • @InstanceOf refuses a test that could never match (#1091). A Grid<Y> extends Node<Y[]> reached from a Node<int[]> crashed the processor, and is now refused at the spec method. Where the test earns no source type, as a Box<?> is no Node<U>, the message says no focus would do and offers another class or a spec. See the message's entry.
  • A method named after a Java keyword takes a trailing underscore (#1056). An enum constant such as NEW or TRUE, a sealed subtype named Default, an @EffectAlgebra permit named Throw, or an imported withDefault crashed the processor. Their methods are now new_(), true_(), default_() and throw_(), and so are @GenerateErrorEnvelope factories. See Prisms.

Effect Paths

Values that are never null

  • A Right or a Success always holds a value (#1065): Either.right, Try.success, their Path, EitherT and ErrorContext factories, and of on their monads, refuse null, as Validated and EitherOrBoth already did. A step that succeeds with nothing to return gives Unit.INSTANCE, as VTask.exec and Path.ioRunnable already do. See Creating Instances.
  • A Left always holds an error (#1077): Either.left, EitherT.left, raiseError on EitherMonad and EitherTMonad, and Maybe.toEither refuse null, as every Effect Path has done since 0.4.11. An error with no detail to carry is Unit.INSTANCE. See Creating Instances.
  • A lazy computation always gives a value (#1078): IO, VTask, VStream, State, Lazy, every other lazy type and their Paths refuse a null value. A value passed in throws at once, and a function that returns null fails the computation when it runs. Only Id keeps a null, for the optics. See A Task Always Gives a Value.
  • Every raiseError refuses a null error (#1078): MonadError.raiseError takes an error that is never null, so a nullness checker flags raiseError(null) on any instance. Maybe, Optional, MaybeT and OptionalT, whose failure carries no detail, take Unit.INSTANCE. See The Solution: raiseError and handleErrorWith.
  • bracketOutcome's release sees its error as a Maybe (#1077): release takes an Either<Maybe<E>, B>. A typed failure arrives as Left(Just(e)), and a failed onDefect as Left(Nothing), where it was Left(null). A release lambda that never reads the error needs no change. See Outcome-aware structured concurrency.
  • The Path SPI answers with an Optional where it has nothing to give (#1078): PathProvider.monadError() and PathRegistry.unregister return an Optional, as PathRegistry.getProvider and GenericPath.monadError() do, where they returned null. See PathProvider SPI Registration.
  • A deferred trampoline step names a supplier that returns null (#1153, #1161): Trampoline.defer, TrampolinePath.defer and Path.trampolineDefer report supplier must not return null when that step runs. The supplier stays lazy.

Removed after their deprecation

  • StateT is built from its state function alone (#445): its constructor, create and StateTKindHelper.stateT take no Monad, and mapT takes only the transformation, as on every other transformer. The record holds only that function, so equals, hashCode and toString leave the Monad out. See Running StateT Computations.
  • Try and TryPath fold failure first, as Either does (#452): the success-first fold, deprecated since 0.4.6, is removed, leaving foldFailureFirst. A call to fold fails to compile rather than running its two functions the wrong way round. See Handling Failures.
  • Each narrow on StateTKind and KindValidator has one name (#1260): narrowK, deprecated in 0.4.5, and narrowWithPattern, deprecated in 0.4.4, are removed. StateTKind.narrow checks the witness at compile time, which narrowK's Kind<?, A> parameter did not. KindValidator.narrowHolder takes what narrowWithPattern took.
  • @PathConfig and the EFFECTFUL and ACCUMULATING capabilities are removed (#1262): all three were deprecated in 0.4.11 because none did what it said. No processor read @PathConfig, and the two capabilities generated what CHAINABLE and RECOVERABLE do, so nothing generated changes. To rename a Path class, set suffix on @PathSource. See Naming and placement.

Resources, cancellation and concurrency

  • A VStream releases its resources when it stops early or fails (#1024): closing reaches every finaliser upstream that the stream has started reading, through any operator, innermost first. takeWhile, zipWith, headOption, find and a failing terminal operation close what they leave unread, so bracket(...).map(f).headOption() releases at once. VStream.closeAfterFailure closes a failed manual pull. See onFinalize: Lightweight Cleanup.
  • VStreamReactive passes cancellation across the bridge (#1024): a toPublisher subscription closes the rest of its stream when it cancels or the stream fails, once it has requested elements. Closing a fromPublisher stream cancels its subscription, so take(n) stops the publisher. See Reactive Interop: Flow.Publisher Bridge.
  • One Resource can serve overlapping uses (#1019): each use releases exactly what it acquired, however the Resource was composed. So a Resource built once with map or flatMap and used nested, or once per concurrent request, no longer releases one resource twice and leaks the other. See Using One Resource Many Times.
  • bracket, guarantee and withResource report a failing cleanup as try-with-resources does (#1053): when a use fails and its cleanup throws too, the use's exception is reported with the cleanup's suppressed onto it. An exception from close() is reported, so a writer whose final flush fails no longer reports success. See The bracket Pattern.
  • A release that blocks finishes after a cancellation (#1057, #1073): it runs with the interrupt status cleared, then restored. That covers a bracketOutcome release, a release after a use that caught a cancellation and returned, and each Saga compensation. A blocking release, such as a refund, used to fail at once. See The bracket Pattern and Saga.
  • A Scope can run again, be retried, or run on several threads (#1052): each run of the VTask from join() opens its own StructuredTaskScope with a new Joiner. A second run used to see the first run's results, and a retry kept failing with the first attempt's error. VResultPath.allSucceed and firstSuccess follow. See Running a Scope Again.
  • Resilience patterns keep a task's exception and its cancellation (#1072): Retry.retryTask, Bulkhead.protect and their Path forms, and CircuitBreaker.protectWithFallback, no longer wrap a checked exception, so retryOn(IOException.class) retries a task's IOException. A cancelled task is not run again or counted by a circuit breaker, and a timeout passes the cancellation on. See Handling Exhausted Retries.

Streams, sagas and stack safety

  • A VStream chunk is a NonEmptyList (#580): chunk, chunkWhile and mapChunked, and chunk on VStreamPath and VStreamContext, hand over a NonEmptyList, so head(), reduce and max on a chunk always give a value. NonEmptyList gains stream(), so a batch function such as chunk -> chunk.stream().map(f).toList() compiles unchanged. See Chunking Operations.
  • A large chunk size or concurrency on a short stream works (#1238, #1249): chunk(Integer.MAX_VALUE), or parEvalMap with that concurrency, ran out of memory before reading an element. Each batch list was sized to the argument, and now starts small and grows. The other chunked and parallel calls, and their VStreamPath and VStreamContext forms, follow. See Chunking Operations.
  • A saga builder with no steps does not compile (#580): SagaBuilder.start() returns a SagaBuilder.Start, which has the step methods but no build(). Each step returns a SagaBuilder whose build() never fails. An empty builder threw IllegalStateException. Steps that come from data are held as a SagaSteps. See Steps From Data.
  • TrampolineUtils.traverseListStackSafe is stack-safe under every applicative (#1018): it combines the effects as a balanced tree, so a million-element traversal under IO no longer overflows the stack. It builds the result in linear time, and a Validated collecting its errors in a list is no longer quadratic in the failures. See Core Components.
  • @PathSource points an effect algebra to FreePath (#1008): a Path over a witness that @EffectAlgebra generates now draws a note at the witness. The algebra has a Functor and no Monad to pass to the Path's of and pure, so the note shows how to wrap its programs in a FreePath. See A witness another processor writes.

Spring

  • A streamed VStreamPath response closes its stream when it stops (#1024): a client disconnect, a timeout or a serialisation failure now runs the stream's finalisers. See VStreamPath: SSE Streaming on Virtual Threads.
  • An SSE stream from a declarative HTTP client closes when you stop early (#1024): headOption(), find(...) or take(n) on a stream from HkjClientExchange.vstream now closes the response. There is no need to drain it first. See Streaming with VStreamPath.
  • An endpoint that sends no body is declared Unit (#1065): a declarative client method returning EitherPath<E, Unit>, VTaskPath<Either<E, Unit>> or MaybePath<Unit> ignores the body. A controller's Unit success writes no body from every handler that writes one value. See Choosing a Return Type.
  • hkj-spring is built against Spring Boot 4.1.1 (#1177): hkj-spring-boot-autoconfigure publishes Spring Boot's BOM as a Gradle platform. Gradle then raises the Spring Boot your application runs with, and each library it manages, to at least 4.1.1's versions; a newer requested version still wins. Spring's dependency-management plugin or a Maven BOM import keeps your version, which must be 4.1.0 or later.

Testing

  • The lens and affine law harnesses check set-set in both orders (#1169, #1175). The two focus values may be given in either order, and an affine is checked on its present target. The harness is stricter: a fixture that broke set-set only in the order left untested now fails. See Optic Laws.
  • Lens and affine law fixtures may include the current focus (#812). assertLensLaws and assertAffineLaws require only that their two focus values differ from each other. A boolean lens can use false, true. See Optic Laws.
  • assertAffineLaws law-checks an affine that writes to an absent focus (#1023): Affines.some() and a Lens.andThen(Prism) affine now pass. On the absent target it checks that modify changes nothing, that set either changes nothing or writes a value that reads back, and set-set. Add assertSetNoOpWhenAbsent for an affine that must leave it alone. See When the focus is absent.
  • The library's own type classes are law-checked more widely (#1151, #1203, #1208, #1212, #1218, #1220, #1222). The Applicative laws now run for List, NonEmptyList, EitherOrBoth and Trampoline, the MonadError laws for CompletableFuture, and the Bifunctor laws for Const over generated values. The build now fails for a Monad with no test class of its own.
  • Showcase tests demonstrate EitherOrBothAssert and NonEmptyListAssert (#1191): they join the hkj-test examples a reader can copy. Property tests in hkj-core and hkj-checker share one set of defaults (#1217).

Build and tooling

  • Checker severity settings and the library's formatting work under every default locale (#1168): severity=WARNING and per-check severity:<id>=WARNING now select warnings under Turkish, where they were silently ignored. Library formatting and generated identifier case conversions use Locale.ROOT. The hkj-core and hkj-examples tests run under an English locale, with Locale.ROOT in more of their formatting (#1160, #1253, #1256).
  • MigrateDeprecationsTo0_5_0 drops StateT's Monad and widens navigated containers (#445, #1106): RemoveStateTMonadArgument drops the Monad from StateT constructor, create, stateT and mapT calls. AddWidenFieldsForNavigatedContainersRecipe adds each container a 0.4.x navigator stepped into to widenFields, and marks one whose elements have a field named like a path method. See 0.5.0 deprecation migration.
  • MigrateDeprecationsTo0_5_0 marks the nulls to replace (#1065, #1077, #1078). DetectNullSuccessValuesRecipe and DetectNullErrorValuesRecipe mark a null given as a success, a value or an error, and a lambda that returns one where it now fails. They also mark Void as a value or error type. They rewrite nothing, and rewriteRun leaves each mark as a comment. See 0.5.0 deprecation migration.
  • MigrateDeprecationsTo0_6_0 replaces a Focus path's untyped shortcuts (#1105): the new group's ReplaceUntypedPathShortcutsRecipe rewrites each call to its typed form, such as each() to each(EachInstances.listEach()). It drops a needless type witness, and marks each call it cannot rewrite, such as one over a List<? extends T>, with its fix. See 0.6.0 deprecation migration.
  • A nullness checker sees every hkj-core package as @NullMarked (#1078): vstream, constant, func, writer_t, effect.spi, util.validation and the remaining optics packages carry the package annotation the others already did. NullAway reading hkj-core from the classpath ignores the module's annotation, so it treated those types as unannotated.
  • A nullness checker reads hkj-test's assertions (#1094): its packages carry @NullMarked, which NullAway reading the jar from the classpath needs. Every assertThat... entry point takes a @Nullable actual, as AssertJ's does, so assertThatEither(result).isNull() passes for a null result, where it threw NullPointerException.
  • Some hkj-test assertions take or give a null (#1094): withMessage, withExceptionMessage, whenForcedThrowsWithMessage and IdAssert.hasValue take a @Nullable expected value. getException() and getValue() on IOAssert and VTaskAssert return @Nullable, since an outcome has only one of the two.
  • A generator extending the built-in Optional, Maybe, List, Set or Collection generator inherits no optic expression (#757): @GenerateFocus recognises those types by name and never asks a generator. So generateOpticExpression() and getRequiredImports() on one return the defaults, and a subclass claiming another type takes no part in @GenerateFocus until it names its own. See Registering your own container type.
  • The state-t-mapt-arity check is retired (#445): every mapT now takes the function alone, so there is no missing argument to report. A disable= or severity: entry that names it has no effect. The kind-value-narrow message now names the right accessor: run() on ReaderT and WriterT, and runStateT(state) on StateT. See Compile-Time Checks.
  • hkj-processor-plugins no longer brings Guava and Jackson 2 onto your annotation processor path (#1177): they came with palantir-java-format, which it declared but never used. The plugin guide's build snippet now names javapoet and the right Avaje SPI artefact. See Step-by-Step Example.

Documentation

The Optics chapter

  • The Optics chapter reads in order, from a first nested update to a PATCH (#1034, #1038, #1056, #1086). The sidebar starts with the pages a newcomer needs, then groups the rest by task. The examples follow one order service, the Mapping capstone's records, and every page keeps its address. See Optics.
  • Find your field, then find your recipe (#1035, #1045). The Focus DSL page answers "what does my field give me?" with a table keyed by the field you hold. The Optics Cookbook opens on a "Find your recipe" table, each recipe leading with a Focus path. See Find your field and Find your recipe.
  • The first lane ends in a capstone and a self-check (#1036, #1039, #1041). What a Path Is Made Of, Updates That Can Fail and Many Edits at Once are rewritten. Six pages pause for checkpoints the build proves, a capstone builds an order desk, and two new tutorials practise them. See Check Your Understanding.
  • Two lookup pages translate what you already write into optics (#1046). Coming from Lombok, Streams and Switch sets each wither, stream and switch habit beside its path, and says when plain Java wins. Coming from Monocle or Haskell maps their names, and the six behaviours that differ. See Look It Up.
  • Compositions read as Java, and two hard rules come with a story (#1037, #1040). The chapter and the hkj-optics skill write Lens.andThen(Prism) = Affine instead of >>>. Composition Rules opens on a short dialogue, and the Lenses page tells how a constructor that lowercases an email quietly breaks a lens. See Optic Composition Rules.
  • Every diagram draws each optic in one shape (#1044, #1049, #1055). A legend on the introduction gives every optic type its own shape, and every diagram fits a phone's screen and describes itself to a screen reader. Each deeper page opens with what you will be able to do (#1042). See How the optic types relate.
  • Production Readiness opens on ten questions and holds the cost facts (#1043, #1050). The questions run from what a lens update allocates to whether generated classes stay stable across versions. Each optic's allocations and reads are stated in one place, naming the method behind each fact. See Production Readiness.
  • Every example is compiled, and every name keeps one shape (#1020, #1047, #1048, #1101, #1114, #1119, #1120). A test holds each value the Optics pages state. A type name such as Order means one thing across the Optics and Mapping chapters, the skills and the annotations' javadoc. See Affines.

Glossary and reference pages

  • The Effect Paths glossary gains nine entries (#1143, #1237, #1245, #1264): CompletableFuturePath, EitherOrBothPath, EitherPath, IdPath, IOPath, LazyPath, MaybePath, OptionalPath and VTaskPath. Each gives a definition and its factories on Path, and four list their main operations. See Effect Paths.
  • The glossary gains entries across type classes, optics and concurrency (#1037, #1184, #1226, #1227, #1235). The type classes gain Alternative, Foldable, MonadZero and Traverse, the optics Getter, Ixed and Each, and the core effect types Reader, State and Writer. Concurrency gains Retry and Saga, and the optics list andThen, modifyF and path widening. See Type Classes.
  • Glossary entries name methods their types have (#1155, #1225): getOptional and build for a Prism, getAll and preview for a Fold, and asFold().getAll(source) for a Traversal. The check that compiles the book's examples now counts every error, where it skipped any that mentioned "preview". See Prism.
  • The cheat sheet and the Type Conversions tables match the code (#1142, #1170, #1171, #1221). The Path Types table gains FreeApPath and VStreamPath, and names the methods that extract a LazyPath, a GenericPath and a WithStatePath. Two conversion rows give the function toTryPath and toIdPath take. See Path Types.
  • The Capability Interfaces page matches the interfaces (#1182): OptionalPath is only Chainable and falls back with orElsePath, Combinable lists zipWith, and Effectful names guarantee. See Capability Interfaces.
  • WriterT joins the transformer overview (#1195): the overview, its Path table and Supported Types now list all six transformers. See WriterT.
  • The hkj-test page lists every assertion class (#1193): EitherOrBothAssert, NonEmptyListAssert, FieldErrorAssert and the Kind-narrowing wrappers join its table, in the book and in the module's README. See What Is Covered.
  • StandardCodecs.locale() says a variant keeps its case (#1202): sl-rozaj and sl-ROZAJ both round-trip, as two different locales, while a case-folded language, region or private-use subtag is still refused. See Standard codecs.
  • The contributing guide clones your own fork, and lists every module (#1194): its first steps now clone your fork, so the later push reaches it, and the module list includes hkj-bom and hkj-test. See Contributing.

Other pages

  • @PathSource has a page of its own (#999): Custom Paths with @PathSource says when a generated Path is worth it over GenericPath, what each capability generates, and how errorType types recovery.
  • The Resource page gives the order finalisers run in (#1019): the order they were added, where it said the reverse. It also says which exception is reported when a release and a finaliser both throw. See Finaliser Behaviour.
  • modifyAllEither says it keeps the first error, not that it stops there (#1031): every focused value is still validated, and the result keeps the first error in traversal order. The javadoc and the examples now say so too. See modifyAllEither: First Error Only.
  • maxNavigatorDepth says what it does (#1030). A value of 1 makes a navigator's own navigation methods return plain paths, and a larger value, the default included, ends no chain. Its javadoc and Tutorial 19 now say so. See Controlling navigator generation.
  • The Focus pages say what a wildcard inside a container's element gives you (#813). A List<Box<? extends Widget>> widens to a TraversalPath over Box<? extends Widget>, wildcard kept. BoxFocus.value() needs a named type argument, so the page shows the lens to write instead. See A wildcard inside the element.
  • Javadoc examples name monoids that exist (#1056): Fold, IndexedFold, TraversalPath, ConstApplicative and the Focus package overview show Monoids.integerAddition() and its siblings, where they named Monoids.intSum() and Monoid.of.
  • Every chapter's Previous and Next links follow its contents (#1086, #1093). Ninety-five pages across eight chapters had stale or missing links, several skipping a page added since. The book check now finds every chapter itself, so a new one is held to its contents too.

Upgrading from 0.4.11

Many changes stop the build, most with a message that names the fix. Read Before you upgrade first, then What a running program can notice, which lists the changes the compiler cannot point at.

Before you upgrade

  • Run MigrateDeprecationsTo0_5_0 before you move to 0.5.0 (#445, #452, #1260, #1262): take hkj-openrewrite 0.5.0 for the run, and keep the project on 0.4.11 until it finishes. Its recipes find every removed API only on 0.4.x. Rename each element field its widenFields recipe (#1106) marks before you move, so the compiler finds the calls through it. See 0.5.0 deprecation migration.
  • Under -Werror, run MigrateDeprecationsTo0_6_0 in the same pass (#1105): on 0.5.0 a call to a deprecated Focus path shortcut fails a -Werror build. Every typed form the recipe writes is in 0.4.11, so it can run beside MigrateDeprecationsTo0_5_0. Replace by hand each call it marks.
  • Replace Void with Unit where a success has nothing to return (#1065, #1078): Either<E, Void> and Try<Void> can no longer hold their null. A VTask<Void>, IOPath<Void> or other lazy computation fails when its step returns null. VTask.exec, Path.vtaskExec and Path.ioRunnable give Unit. The MigrateDeprecationsTo0_5_0 run marks each site.
  • Recompile what was compiled against a generated Focus class (#1033, #1214): a field or a hop that gains a navigator returns a navigator class. A module compiled against the earlier class fails with NoSuchMethodError until it is rebuilt.
  • Recompile what calls chunk, chunkWhile, mapChunked or SagaBuilder.start() (#580): start() returns a new type, so a module compiled against 0.4.11 fails with NoSuchMethodError. A chunk keeps its erased type, so such a module links, then fails with ClassCastException where it reads a chunk as a List.

What a running program can notice

Mapping

  • A date codec over a formatter that cannot parse what it writes throws when built (#1144): StandardCodecs.localDate(formatter) and offsetDateTime(formatter) throw IllegalArgumentException, with the formatter's DateTimeException as its cause. Such a codec used to be built, then refuse every input.
  • A bean property is read through its accessor, never a word declared before it (#1128): a getaway() or island() declared before getAway() or isLand() read away or land, and the accessor now does.
  • A merge fill can parse through a generic spec in this compilation (#1123): a dependency's spec for exactly the pair used to fill it. It now resolves as a mapping's nest does, your own specs first. A leaf named after the component, delegating to the dependency's Impl, keeps the old fill.

Optics

  • A @GenerateFocus record field, or a container of one, returns a navigator (#1033, #1103, #1214): a getClass() check sees the navigator class, as it does the form a hop from a wider navigator returns. A navigator equals only a navigator of the same class over an equal path, never the plain path it wraps.
  • A navigator is a path (#1033): one from a record that wrote generateNavigators = true passes instanceof FocusPath, and an overload taking a FocusPath is chosen over one taking Object.
  • A 0.4.x navigation call can compile against a path method instead (#1033, #1106, #1243): take a record that writes generateNavigators = true, with a Map field byTicker of records with a last field. byTicker().last() then calls the path's own last(), which throws ClassCastException. A note at the field and MigrateDeprecationsTo0_5_0 ask for last to be renamed before you move. See Compiler Messages.
  • A null @Nullable container has no focus (#1062): a generated Focus path that steps into one reads nothing, and a modification leaves it null, where both threw NullPointerException.
  • A read through an Optional or a Maybe reads a null focus as absent (#767): Fold.preview, TraversalPath.preview and toMaybePath, and FocusPath.toMaybePath used to throw NullPointerException. So did FocusPath.asAffine, headOption, IxedInstances.get and the list optics listAt, listHead and listLast, whose setters, and the list prisms' build, now write a null.
  • Other optics carry a null focus through (#767): find passes over a null focus to the next match, where it threw. Setter.forList, and FocusPaths.listElements, arrayElements, listCons, listSnoc, listTail, listInit and mapValues, carry a null element. IxedInstances.contains counts an index holding a null as present, and Plans.preflight walks a batched traversal to the end, so truncated is now false.
  • A traversal's Effect Path bridges leave a null focus out (#1017): toStreamPath, toVStreamPath and VStreamPath.fromEach used to stream it, and toListPath and toNonDetPath threw NullPointerException. TraversalPath.fold counts it as the monoid's empty value, where Monoids.string() appended "null". getAll still keeps every focus.
  • A raw modifyF under Either or Try cannot pass over a null element (#1065): filtered keeps an element it passes over by lifting it with of, as do a prism and an affine that miss. Under those applicatives of now refuses null. TraversalExtensions.modifyAllEither and its siblings keep the element.
  • OpticPrograms.get fails on a null focus, naming preview (#1078): a Free program always gives a value, so the direct and logging interpreters throw NullPointerException, and ValidationOpticInterpreter records an error. StateOps.view on a null focus fails the same way.
  • A map an optic writes iterates in the source's order (#1022): it came back in hash order, so printed output or a test of a map's iteration order can change. FocusPaths.mapValues over an empty map returns a new map.
  • A TreeMap keeps its order but not its type (#1022): forMapValuesCollecting(TreeMap::new) keeps both when the keys sort in their natural order. A TreeMap with its own comparator needs a collector that builds the new TreeMap with that comparator.
  • The function a predicate does not pick is no longer called (#1026): by modifyWhen and branch on a Traversal, and by modifyWhen and modifyBranch on a Lens. Lens.setIf no longer calls set on a failed predicate. Traversals.speculativeTraverseList is unchanged: it applies both functions to every element.
  • A query's predicate is not called after its answer (#1056): find, exists and all, on Fold, IndexedFold and TraversalPath, stop at the focus that settles them, so a logging or counting predicate sees fewer calls. A Fold of your own whose foldMap catches every exception still gives the same answer, but walks on to its end.
  • A read no longer runs a traversal's rebuild (#1056): a Traversal of your own whose rebuild logs or counts no longer runs it for Traversals.getAll, a TraversalPath's reads, IndexedTraversals.toIndexedList, getAll and length, or asIndexedFold().
  • OptionalTraverse.traverse rejects a function that returns null (#1056). For a present value, a null from the function used to give an empty Optional. It now throws NullPointerException, as the other Traverse instances do.
  • Sequenced lists are unmodifiable at every size (#1018): IndexedTraversals.sequenceList, ForTraversal.toList and ForIndexed.toIndexedList now return one, as Traversals.traverseList does. So do IndexedTraversals.forList on a non-empty list and Traversals.traverseArray on an empty array. Copy the result into a new ArrayList to change it.
  • ForTraversal.toList and ForIndexed.toIndexedList honour the filters (#1018): they collect only the elements that pass filter and filterIndex, as modify and set already did. They used to return every focused element. See Collecting Results.
  • The list optics that returned an ArrayList now return an unmodifiable list (#1063): .each(), .at(i), .head() and .last() on a Focus path, ListTraversals, AtInstances.listAt and listAtWithPadding, and IxedInstances.listIx. Changing one throws UnsupportedOperationException, so copy it first. With nothing at the index, .at(i) and listIx return your own list. See What each collection optic builds.
  • A Focus path over a field declared as ArrayList fails when .each(), .at(i), .head() or .last() writes (#1063): storing the rebuilt list, an unmodifiable List, back throws ClassCastException. .each() already failed on a one-element list. Declare the field as List, or use each(EachInstances.fromIterableCollecting(ArrayList::new)).
  • A @ViaConstructor lens with no parameterOrder sets through its constructor (#906): the order is read from the constructor's parameter names, where set used to throw UnsupportedOperationException.
  • A spec lens whose focus is wider than its getter's wrapper passes the value as it is (#906): with Number over an int getter, it used to be cast to int for withX(int). Where the wither, setter or constructor is also overloaded for a wider type, such as withX(Object), that overload is now the one called.
  • A @ViaCopyAndSet lens over a withX setter hands back what it returns (#1108, #1128): not the copy, where every method of that name the new value can be passed to returns the source type. A fluent withX behaves as before, and so does every set method. A witheMail, as JavaBeans spells one for eMail, is a withX setter too.
  • A @ViaBuilder lens over a Lombok @Singular collection, or through a protobuf-java message's addAllTags or putAllPoints, replaces the collection (#1110): set used to add the new elements to the ones toBuilder() copied. Code that set a one-element list to add an element now keeps only that element; append with modify instead.
  • An Each of your own that declares eachWithIndex() without @Override is no longer indexed (#1261): it still compiles, as a method of its own, but supportsIndexed() now returns false for it. Implement EachIndexed instead.

Effect Paths: null values

  • A null success throws, or fails a Try (#1065): Either.right(null), Try.success(null) and the Path factories throw NullPointerException, and so does an Either or EitherPath map whose function returns null. In Try.of, map, ap, recover and TryPath, a null result gives a Failure holding one. A VResultPath fails when it runs.
  • A null error throws (#1077): Either.left(null), EitherT.left, raiseError(null) on EitherMonad and EitherTMonad, and a mapLeft or bimap whose error function returns null throw NullPointerException, where they built Left(null).
  • raiseError(null) throws on Maybe, Optional, MaybeT and OptionalT (#1078): it gave Nothing or an empty Optional. Pass Unit.INSTANCE. Every MonadError now refuses a null error.
  • recover(ma, null) throws on EitherMonad, TryMonad and EitherTMonad (#1065): whether or not ma failed. A null fallback used to give Right(null) or Success(null). Try.orElseGet refuses a supplier that returns null, as Validated.orElseGet does; orElse(null) still hands a null back.
  • The conversions into an Either check their error even when it is not needed (#1077): Maybe.toEither checks its error, or its supplier, even on a Just. The optics helpers getEither, getValidated, modifyEither and modifyValidated check theirs even when the focus is there. Try.toEither throws NullPointerException for a mapper that returns null, where it threw KindUnwrapException.
  • The one-argument toEitherPath and toTryPath on a FocusPath throw on a null focus (#1065): they gave Right(null) and Success(null). The message names the form that takes an error. EitherPath.focus(FocusPath) throws on a null focus too, naming focus(path.nullable(), …), and TryPath.focus gives a Failure.
  • ErrorContext.io treats a null result as a failure (#1065): its error mapper sees a NullPointerException, where the context held Right(null).
  • A function that returns null fails the computation (#1065, #1078): a callable, supplier, map function, combiner or recovery function that returns null fails the lazy computation when it runs, and the NullPointerException names that function. So VTask.of(() -> { work(); return null; }) fails, where run() handed the null back.
  • A null result fails where a computation becomes a Try or an Either (#1065): runSafe, toTryPath, asTry, Scope.joinSafe and EffectBoundary.runSafe give a Failure holding a NullPointerException. joinEither and FreePath.attempt give a Left, and catching hands it to its mapper.
  • A null value passed to a lazy factory throws (#1078): VTask.succeed, State.pure, Reader.constant, Writer.value, Lazy.now, Trampoline.done, Free.pure, FreeAp.pure, VStream.of, Context.succeed, the Path factories such as Path.ioPure and Path.id, and of on their monads throw NullPointerException. So do the constructors of StateTuple, Writer, Trampoline.Done, Free.Pure and VStream's Emit and Seed.
  • StateT, ReaderT and WriterT refuse a null value (#1078): of on their monads, WriterT.of and WriterT.writer throw on one. A function given to ReaderT.reader, or to map or ap on their monads, that returns null fails the computation. ReaderTMonad.of(null) over Optional gave an empty Optional.
  • VTask.run() and runAsync() refuse a null from a hand-written task (#1078): a VTask written as a lambda that returns null throws NullPointerException from run(), and fails the future from runAsync(). PathOps.firstVTaskSuccess counts such a null as a failure and tries the next path. VTaskContext.runOrElse refuses a null default, and runOrElseGet a handler that returns null.
  • An IOPath, Par and a Scope refuse a null from a hand-written IO or task (#1078): IOPath.unsafeRun() throws NullPointerException for one, as does each step that runs it, and EffectBoundary.embed. Par's combinators and a Scope that needs every subtask fail on such a null, and an accumulating Scope records it as an error.
  • A stream refuses a null element (#1078): VStream.of throws on one, and fromList, fromIterator, fromStream and generate fail the stream when they pull one. fromPublisher cancels its subscription and fails the stream. StreamPath does the same for its sources. PathOps.traverseVStream still hands a null item in its list to the function.
  • A CompletableFuturePath fails on a future that completes with null (#1078, #1072): Path.future and fromFuture check the value as it arrives: map a runAsync future to Unit first. Cancelling either future cancels both. withTimeout and completeOnTimeout complete only the new path's future, and the latter refuses a null default. CompletableFutureMonad keeps the JDK's null.
  • An IdPath always holds a value (#1078): Path.id(null) throws, as does Path.idPath given an Id holding null. FocusPath.toIdPath throws on a null focus, naming toMaybePath, and so does IdPath.focus, naming focus(path.nullable()). Id and IdMonad still hold a null, since every optic's modify runs through them.
  • The lazy Paths' focus refuses a null focus (#1078): IOPath, VTaskPath and VStreamPath fail when they run, and the message names the path.nullable() form to use. They used to hold the null.
  • Context.ask fails on a key bound to null (#1078): read a key that may hold null with asks. SecurityContext.requireAuthenticated still fails an anonymous user with UnauthenticatedException.
  • A null in a saga fails its step (#1065, #1078): a step whose task returns null fails like any other step, and so does a map that returns null. The steps before it are compensated, under Saga.run and Saga.runSafe alike. A null result gave Right(null) from runSafe, and run handed it back.
  • A null result from a deferred trampoline step names its supplier (#1153, #1161). It is still a NullPointerException, raised when the step runs: from run() for Trampoline.defer, and from the deferred step for TrampolinePath.defer and Path.trampolineDefer. No call site needs to change.

Effect Paths: resources, cancellation and streams

  • A finaliser that used to be skipped now runs (#1024): when a VStream stops early, a terminal operation fails, or a function given to an operator such as map, filter or mapTask throws. A finaliser that throws then fails the operation that stopped, such as take or headOption. Closing runs the innermost finaliser first, as completing does.
  • A bracket whose use function throws releases the resource (#1024): before the failure reaches the caller. It used to leave the resource acquired.
  • Closing a pulled bracket stream releases its latest run (#1024): closing the head of a bracket after a pull releases the resource that pull acquired, where it used to release nothing.
  • onFinalize runs its finaliser for each consumption of a VStream (#1018): a second consumption, or one after close(), used to skip it. It now also runs when the source fails as it is pulled. close() runs it for the latest consumption only, unless that consumption has run it already. See onFinalize: Lightweight Cleanup.
  • VStream.defer calls its supplier when the pulled VTask runs (#1024): not when pull() is called, and again on each run. generate, iterate and unfold call their functions then too. distinct, chunk and chunkWhile now give the same step each time a pulled VTask runs, and a distinct tail pulled again replays its elements.
  • VStreamPar.parEvalMap, parEvalMapUnordered and the VStream applicative's ap read their input when the pulled VTask runs (#1024): not when pull() is called, and afresh on each run.
  • VStreamPar.merge starts a producer for each run of its pulled VTask (#1024, #1073): not when pull() is called. Closing it stops that run's producer and waits for each unfinished source to close, even with an interrupt pending. close() now stops the producer when its VTask runs, not when it is called. See merge: Concurrent Multi-Source Consumption.
  • Cancelling a toPublisher subscription runs the stream's finalisers (#1024): once the subscriber has requested elements. It used to leave the stream open.
  • VStreamThrottle.throttle gives each consumption its own window (#1018): each consumption's first emission opens its first window. A later or concurrent consumption used to share the first one's window.
  • Resource.onFailure runs its action when the use fails (#1019): it used to do nothing. The action runs before the release, both when the use fails and when a step composed after it fails while the resource is held, such as map's function throwing. See onFailure Callback.
  • Resource.map releases the resource when its function throws (#1019): it used to leave the resource acquired.
  • A Resource rejects a null value (#1019): an acquire given to make or fromAutoCloseable that returns null, a null passed to pure, and a function given to map that returns null now fail with NullPointerException. Hold an absent value as a Maybe instead. See Factory Methods.
  • A failing use keeps its own exception when cleanup fails too (#1019, #1053): in a Resource, and in IOPath.bracket, bracketIO, guarantee, guaranteeIO and VTaskPath.guarantee, the cleanup's exception is kept among the use's suppressed exceptions. When a release and a finaliser both throw, the release's exception is reported. See Exception Safety.
  • and(second, third) releases its first resource when two releases throw one exception (#1019): the same exception instance from both used to fail with IllegalArgumentException and leave the first resource acquired.
  • An exception from close() fails withResource, withResourceIO and fromAutoCloseable (#1053): it used to be dropped. After a successful use, IOPath throws it unchecked, a SQLException as the cause of a RuntimeException, and the task from Resource.use fails. After a failed use, it is suppressed onto the use's. To ignore it, pass bracket or Resource.make a release that catches it.
  • withResource and withResourceIO reject a null resource (#1053): a resource supplier that returns null now fails with NullPointerException before the use runs. The use used to receive the null. Check for an absent resource before building the path.
  • Cleanup runs with the interrupt status cleared (#1019, #1053, #1057, #1073): after any use, in Resource, IOPath, VTaskPath, VStream finalisers, bracketOutcome and each Saga compensation. So a blocking close is not cut short, and a Scope that timed out during the use waits for it. The status is restored afterwards, and withResource sets it when close() throws an InterruptedException.
  • A cancelled bracketOutcome use leaves the thread interrupted (#1057): when the use throws an InterruptedException, the interrupt status is set again once the release has run. The thread's next blocking call then throws InterruptedException at once. To tell a cancellation from other failures, map InterruptedException to its own error in onDefect.
  • A task's checked exception passes through Retry, Bulkhead and protectWithFallback unwrapped (#1072): Bulkhead.protect, Retry.retryTask and its fallback and recovery forms, and withBulkhead and withRetry on VTaskPath and VResultPath, wrapped it in VTaskExecutionException. CircuitBreaker.protectWithFallback wrapped it, and an Error, in RuntimeException. A retryOn predicate, a fallback and RetryExhaustedException.getCause() now see the exception itself.
  • An interrupt stops a retry (#1072): an attempt that throws InterruptedException is not tried again, whatever the predicate, and the exception passes through as thrown, past any fallback. A thread left interrupted between attempts stops the retry with its status set and RetryExhaustedException, which a fallback still handles. A recovery task does not start after either.
  • A timeout passes a cancellation on (#1072): when a thread waiting on VTask.timeout is interrupted, so is the task, and InterruptedException follows once it stops or the time runs out. It used to leave the task running and report its result or a TimeoutException. withTimeout on VTaskPath, IOPath, VResultPath and EitherPath builds on it.
  • A cancelled call trips no circuit breaker and skips a ResilienceBuilder fallback (#1072): a call that throws InterruptedException counts as neither a success nor a failure, whatever recordFailure says. The fallback lets it pass.
  • VTask.run(), runSafe() and Try.attempt keep a cancellation's interrupt status (#1072): each sets it again for an InterruptedException it wraps or returns as the failure, and the Path forms built on them follow. So a retry around IOPath.withTimeout sees the cancellation and stops, and the thread's next blocking call throws at once. The status used to be cleared.
  • An extremely long duration no longer throws (#1072): a duration too long to count in nanoseconds, such as ChronoUnit.FOREVER.getDuration(), counts as about 292 years in timeouts, waits, retry delays, throttle windows and retryAfter. Some of those threw ArithmeticException. Sub-millisecond parts are kept, where RetryPolicy and several waits dropped them.
  • Scope.firstComplete and ScopeJoiner.firstComplete() cancel the other subtasks once one completes (#1052): they used to wait for every subtask. Those still running are interrupted and those not yet started never start, so work a slower subtask does afterwards, such as writing a cache, no longer happens. Move work that must finish out of the race.
  • ScopeJoiner.joiner() returns a new Joiner on each call (#1052): code that opened a scope with one call and read result() from another now reads a Joiner no scope has used. That gives an empty result or an exception, so use the value the scope's join() returns.
  • A Scope's timeout cancels its subtasks (#1052): when it expires, those still running are interrupted, and the run fails with a TimeoutException once they have stopped. So a subtask that ignores interrupts now delays the failure. Its subtasks now see the caller's ScopedValue bindings, which they used to find unbound.
  • An interrupt while a timed Scope waits fails the run (#1052): with an InterruptedException, as it does for an untimed Scope. A timed one used to ignore it and keep waiting.
  • Par and PathOps copy the list they are given when called (#1052): Par.race, all and traverse, and PathOps.sequenceVTask, traverseVTask, firstVTaskSuccess, parSequenceIO and raceIO. Changing the list afterwards no longer changes what a run does. A null task or path in a list, or in Scope.forkAll's, is rejected then, not when it runs.
  • Scope.named names the scope in a JSON thread dump (#1052): the name used to be dropped. named(null) now throws NullPointerException, where it was accepted.
  • A chunk is not a List (#580): what chunk, chunkWhile and mapChunked hand over prints as NonEmptyList[1, 2] and never equals a List, even one holding the same elements. Compare chunk.toJavaList() where code checked a chunk against a List.
  • A large chunk size or concurrency on a short stream returns its elements (#1238, #1249): chunk(Integer.MAX_VALUE) and parEvalMap with that concurrency used to throw OutOfMemoryError before reading an element. The other chunked and parallel calls, and their VStreamPath and VStreamContext forms, now return their elements too. A size that already returned chunks returns the same ones.

Effect Paths: lists and StateT

  • StateT equality leaves out the Monad (#445): two StateT values wrapping the same function instance are equal, with equal hash codes, whichever Monad built them. toString prints only runStateTFn, so a test comparing the old text fails.
  • A list from PathOps or Par cannot be changed (#1017): the sequence and traverse methods, Par.all and Par.traverse handed back their working ArrayList. So changing the list inside an EitherPath changed what every later run() saw. Changing one now throws UnsupportedOperationException, so copy it into a new ArrayList first.
  • TrampolineUtils.traverseListStackSafe and sequenceStackSafe return an unmodifiable list (#1018): as Traversals.traverseList does. Changing it throws UnsupportedOperationException, so copy it into a new ArrayList first.

Spring

  • The declarative HTTP client refuses an empty body for EitherPath and VTaskPath (#1065): it gave Right(null). It now throws an EmptyResponseBodyException, or fails the task, after the request has been made. A retry policy that retries every throwable would send it again, so exclude that type. Declare Unit where the endpoint sends no body.
  • A Unit success writes no body (#1065): a handler that writes one value wrote Unit as the JSON string "INSTANCE". It now sends the resolved status with no body, as a void controller method does.
  • A lazy Path whose step returns null writes the failure status (#1078): an IOPath, VTaskPath, CompletableFuturePath or FreePath handler wrote null as the body. The step now fails, as a null does in any lazy computation.
  • A JSON Either holding "right": null or "left": null is refused (#1065, #1077): EitherDeserializer reports it as bad input, as ValidatedDeserializer does for a null value.
  • An SSE frame holding null fails the stream (#1078): a stream from HkjClientExchange.vstream fails with SseStreamException, as for any other frame it cannot read.

Testing

  • The lens and affine law harnesses check set-set in both orders (#1169, #1175). A fixture that broke set-set only in the untested order now fails. A lawful lens or affine still passes with the values either way round.
  • Lens and affine law harnesses accept one value equal to the current focus (#812). Existing lawful fixtures still pass, and two equal supplied values are still rejected. A setter that ignores updates still fails set-get on the other value.
  • assertAffineLaws no longer requires set to leave an absent target alone (#1023): a test that relied on it to catch an affine writing there now passes. Add AffineLaws.assertSetNoOpWhenAbsent(affine, absentSource, value) to keep that check.

Build and tooling

  • Uppercase warning settings take effect on Turkish systems (#1168): remove any workaround that lowercased WARNING before passing it to the checker. Generated for-comprehension lambda names now use i rather than dotless ı; tuple constant names are unchanged. Numeric log counts use the root locale's digits. No source changes are required.

What stops a build that compiled

AreaNow fails the buildDo thisIssue
MappingA @GenerateMerge fill whose pair two generic specs in this compilation both cover, where neither is more specific, which a dependency's spec for exactly the pair used to fillAdd the leaf the error spells, delegating to the spec you mean, or declare a spec for exactly the pair in this compilation#1123
MappingA bean with a method that only starts like an accessor, such as getx() and setx(int) for x, where nothing else reads or writes the component in its place: getaway() beside setAway(String) alone, or getid() read through a @ReadOnly markerRename it as the error offers, such as getX() or getId(). Where it is not meant to carry the component, name its property in an @Unmapped marker, where the error offers one#1128
MappingA PATCH bean's setter with no getter spelt seteMail(String), as JavaBeans spells one for eMail, or set_links(String)Remove the setter, or rename it after the component it should update and give it a getter, or name its property in an @Unmapped marker#1128
OpticsUnder a nullness checker, FocusPaths.nullable() assigned to an Affine<X, X>Declare Affine<@Nullable X, X>, the type Affines.nullable() returns#767
OpticsUnder a nullness checker, AtInstances.listAtWithPadding(null) for a non-null element typePad with a value, or name a nullable element type: AtInstances.<@Nullable String>listAtWithPadding(null)#767
Opticsnullable() whose result is bound to another focus type, as in AffinePath<S, Integer> x = stringPath.nullable(), which compiled as an unchecked castConvert the focus with a Getter or an Iso composed after nullable()#1017
OpticsCode that holds or chains the Focus path of a field reaching a container whose TraversableGenerator names no optic expression, as the AffinePath or TraversalPath it widened to: its declared type, a call such as getAll or via, or a navigator method through itUse the path the Focus method now returns, which stops at the container, and widen it with .some(affine) or .each(each); or override generateOpticExpression() in the generator#1059
OpticsA prism onto an inner class of a generic class, used at its raw type: in a type argument, as in Prism<Outlines.Outline, Outlines.Dot> p = OutlinePrisms.dot(), or read back as a particular Outlines<String>.DotWrite Outlines<?>.Dot or use var; where the value must be an Outlines<String>.Dot, cast to it under @SuppressWarnings("unchecked"), which states the unchecked step#886
OpticsAn @InstanceOf target that is an inner class of a generic class, where the source does not pin an enclosing argument that the focus names, such as Outer<String>.Plain from a Node<String>, or that bounds a pinned argument, as Z extends X does, either of which compiled as an unchecked narrowingDeclare the focus with a wildcard there, Outer<?>.Plain, or narrow with @MatchWhen#886
Opticsvar x = iso.andThen(traversal); (or traversal.andThen(affine) or traversal.andThen(iso)) followed by assigning another kind of optic to x; your own use(Optic) and use(Traversal) overloads now pick the Traversal oneDeclare x as the Optic you mean to hold, or call the overload you mean#1021
OpticsA @ViaConstructor parameterOrder that passes a value where the constructor's parameter names say another belongs, such as {"y", "x"} for Point(int x, int y), including a lens named after an accessor no parameter is named afterWrite the order the error offers, which follows those names, or rebuild with the strategy it names#906
OpticsA @ViaConstructor lens with no parameterOrder, whose set threw, where the constructors' parameter names give no order the call rebuilds through, a longer constructor might hold more, the lens's focus fits no constructor, or the source type is abstractWrite the order, declare the focus the error names, or rebuild with the strategy it names#906
OpticsA spec lens whose focus is wider than its getter's wrapper, such as Number over an int getter, rebuilding through an overloaded wither, setter or constructor none of whose overloads takes the focus; setting a value of another kind used to throw ClassCastExceptionDeclare the focus as the wrapper, Integer#906
OpticsUnder -Werror, a written @ViaConstructor parameterOrder that passes nothing for a value a longer constructor, or a record component, takes, such as {"amount"} beside Fee(long amount, String currency)Write the order the warning offers, or annotate the lens method @SuppressWarnings("shorter-constructor") where the shorter constructor works the value out#1067
OpticsA @ViaConstructor parameterOrder that passes a value where a variable-arity constructor's parameter names say another belongs, such as {"note", "label"} for Sticker(String label, String note, String... parts)Write the order the error offers#1067
OpticsUnder -Werror, a @ViaCopyAndSet setter spelt as a wither whose result the lens cannot keep, such as Shrub withTitle(String) on a Hedge extends Shrub, or a fluent withX returning a raw or base typeAnnotate the lens method @SuppressWarnings("dropped-result") where the method sets the copy it is called on; otherwise follow the warning's fix#1108
OpticsA spec lens whose generated code made an unchecked conversion or call, which compiled, with javac's note on unchecked operations, unless -Xlint:unchecked and -Werror were both on: an accessor that reads a raw type for a focus with type arguments, a focus written raw for a method or constructor that takes one with them, a raw accessor passed to a @ViaConstructor parameter that has them, a method called through a raw builder or a raw supertype, or a focus written raw for a method that takes the type a wildcard of the source or its builder stands forRead and rebuild through members that keep their type arguments, or declare the lens over the raw type with @SuppressWarnings("rawtypes") on the lens method, or declare the spec with a type argument in place of the wildcard, or point toBuilder at a method that hands back the builder with its type arguments, or rebuild with another strategy#905, #1110
OpticsWith widenCollections = true, a Focus method over an array of a reference type, or over a container holding one, such as String[] tags or List<String[]> rows, used as its old path or chained with .each(EachInstances.arrayEach()). A String @Nullable [] was an AffinePath to the arrayDrop the .each(EachInstances.arrayEach()) and use the path over the elements, or keep the array's own path from its lens, FocusPath.of(SLenses.tags()), adding @GenerateLenses if the record lacks it#1062
OpticsWith widenCollections = true, an array whose element is an SPI container, such as an Either or a Map, with a wildcard argument or none, such as Either<?, Widget>[] or Either[]Name the element's type arguments, as in Either<String, Widget>[], or drop @GenerateFocus and add or keep @GenerateLenses, as for an Either<?, Widget> component#1062
OpticsA record that writes generateNavigators = true and holds another @GenerateFocus record, or a List, Optional or other container of one, that has a field named like a no-argument path method, such as last, head, each, segments, toLens or toPathRename that field, or keep the field that navigates into it a plain path, as the error says#1033, #1103
OpticsA @GenerateFocus includeFields, excludeFields or widenFields name that matches no component of the record, which changed nothing, or, as the only name in includeFields, left every field without a navigatorWrite the component's name, as the error offers, or take the name out#1104
OpticsUnder -Werror, a @GenerateFocus field list that changes nothing: excludeFields beside a non-empty includeFields, includeFields or excludeFields with generateNavigators = false, widenFields beside widenCollections = true, or a widenFields name whose field is widened anyway or holds no containerRemove the list or the name, or the setting that leaves it unread, as the warning says#1104
OpticsUnder -Werror, an includeFields name matching a component that has no navigator, such as a String, an unannotated record or an unwidened Map of annotated records, including wildcard targets and wildcard containersFollow the warning's fix line: name a navigator field, annotate the target record, widen the container, or remove includeFields and turn navigators off#1181, #1201
OpticsWith generateNavigators = true and no widenCollections, a Map or a third-party collection of @GenerateFocus records, such as ImmutableList<Position> positions, used as the navigator over its elements: .positions().ticker(), getAll, modifyAll, count, or assigned to a TraversalPathRun MigrateDeprecationsTo0_5_0 (Before you upgrade), or add the field to widenFields, as the note at the field says, or set widenCollections = true#1033, #1106
OpticsA Focus method's result for a field of another @GenerateFocus record, or of a List, Optional or other container of one, with its type inferred: a var then assigned another path, such as lines = lines.filter(p), or a generic call such as Stream.of(OrderFocus.customer()).toList() assigned to a List<FocusPath<Order, Customer>>. A navigator's hop into one record from a wider path, such as orders().customer(), is inferred the same wayName the path type: declare the variable, or give the call its type argument#1033, #1103, #1214
OpticsOn a record that writes generateNavigators = true, two components that both reach @GenerateFocus records and whose names differ only in the case of their first letter, such as items and Items. A List, Optional or other container of one now gets a navigator too. Both navigator classes would be ItemsNavigator, so the processor refuses the later one. On the default, the later one keeps its plain path, with a noteRename one of the components, or keep the later one a plain path, as the error's fix line says#1103, #1259
OpticsCode calling a lens @ImportOptics generated through a word, such as draw() through withdraw(long) beside getDraw(); a class whose only wither was such a word is now refusedDrop the call: withdraw never set draw. Where the class has a method that does set the field, name it with @Wither in a spec interface#1128
OpticsA call to a Focus path's each(), some(), at(index), atKey(key), traverseOver(traverse), head(), last(), tail(), init(), cons(), snoc(), headTail() or initLast() (a [removal] warning)Run MigrateDeprecationsTo0_6_0, or use the typed form that Deprecated for removal in 0.6.0 names#1105
OpticsA call to Each.eachWithIndex(), deprecated since 0.4.7, or an @Override of itDeclare the instance as EachIndexed, the type EachInstances.listEach() returns, and call indexedTraversal(), which returns the traversal itself rather than an Optional. Where you hold an Each you cannot redeclare, match it with instanceof EachIndexed<?, S, A> indexed. For an Each of your own with an index, implement EachIndexed#1261
Effect PathsA Monad passed to new StateT<>(fn, monad), StateT.create, StateTKindHelper.stateT or StateT.mapTRun the recipe as Before you upgrade says, or drop the argument by hand#445
Effect PathsA two-component StateT record pattern, or StateT::create or StateT::new as a BiFunctionMatch the one component, or take a Function, by hand#445
Effect PathsA StateT in var or a chained call over a witness with a type argument, such as EitherKind.Witness<E>, which now infers Object for itName the types, as in StateT.<S, EitherKind.Witness<E>, A>create(fn), or declare the variable#445
Effect PathsevalStateT and execStateT without a Monad, deprecated since 0.4.6Pass the outer Monad<F>, the one Instances.stateT took, as the last argument#445
Effect PathsStateT.monadF(), deprecated since 0.4.6Keep a reference to the outer Monad<F> you built the stack with, and use it in place of the call#445
Effect PathsTry.fold or TryPath.fold, deprecated since 0.4.6Run the recipe as Before you upgrade says, or call foldFailureFirst with the two functions swapped#452
Effect PathsStateTKind.narrowK, deprecated since 0.4.5, or KindValidator.narrowWithPattern, deprecated since 0.4.4Run the recipe as Before you upgrade says, or rename the call to narrow or narrowHolder. Declare an argument typed Kind<?, A> as the Kind<StateTKind.Witness<S, F>, A> that narrow takes#1260
Effect Paths@PathConfig on a package, deprecated since 0.4.11, which now fails with cannot find symbolRun the recipe as Before you upgrade says, or delete it and its import, since it changed nothing generated#1262
Effect PathsEFFECTFUL or ACCUMULATING, deprecated since 0.4.11, wherever it is named, as in @PathSource or a switch over PathSource.CapabilityRun the recipe as Before you upgrade says, or write CHAINABLE for EFFECTFUL and RECOVERABLE for ACCUMULATING, which generate the same class. In a switch that already lists the replacement, delete the case#1262
Effect PathsA call to ScopeJoiner.resultEither() (a [removal] warning)Use joinEither() on the Scope, as Deprecated for removal in 0.6.0 says#1052
Effect PathsUnder a nullness checker, a nullable value passed to Either.right, Try.success, Path.right, Path.success, TryKindHelper.success, EitherT.right, the Either.Right or Try.Success constructor, or of on EitherMonad, TryMonad or EitherTMonadPass Unit.INSTANCE for a step with nothing to return, or route a value that may be missing through Maybe#1065
Effect PathsUnder a nullness checker, a nullable error passed to Either.left, EitherT.left, the Either.Left constructor, or raiseError on EitherMonad or EitherTMonadPass an error that says what went wrong, or Unit.INSTANCE where the error type is Unit#1077
Effect PathsA bracketOutcome release whose outcome parameter is declared Either<E, B>, or that reads the error as an ETake Either<Maybe<E>, B>, and read the error through the Maybe#1077
Effect PathsUnder a nullness checker, a nullable value passed to a lazy value factory such as VTask.succeed, State.pure, Lazy.now or Path.ioPure, or to of on their monads or on a transformer's, or a hand-written VTask, IO or Reader lambda that returns onePass Unit.INSTANCE for a step with nothing to return, or route a value that may be missing through Maybe#1078
Effect PathsUnder a nullness checker, a nullable error passed to raiseError on any MonadError, such as MaybeMonad or OptionalMonadPass an error that says what went wrong, or Unit.INSTANCE where the error type is Unit#1078
Effect PathsA PathProvider that overrides monadError(), or code that reads monadError() or PathRegistry.unregister as a value that may be nullReturn Optional.of(...) from the override, and read the Optional#1078
Effect PathsUnder a nullness checker reading hkj-core from the classpath, a nullable argument to a type it treated as unannotated: VStream, Const, WriterT, the Path SPI or the validation helpersPass a value, or route one that may be missing through Maybe#1078
Effect PathsCode that treats a chunk from chunk, chunkWhile or mapChunked as a List: a type such as VStream<List<A>> or List<List<A>>, a reference to a method taking List<A>, such as VStream::fromList, a List method such as get, getLast or isEmpty, or a stored Function<List<A>, List<B>> passed to mapChunkedDeclare NonEmptyList<A>, and use head() and last() for get(0) and getLast(). Pass chunk.toJavaList() where a List is needed, add .map(NonEmptyList::toJavaList) to keep a VStream<List<A>>, or pass a stored function as fn.compose(NonEmptyList::toJavaList)#580
Effect PathsA variable, var included, or a parameter that holds SagaBuilder.start(), such as one a loop adds steps toDeclare it a SagaSteps, the type both stages share, and switch on it to build, as Steps From Data shows#580
Effect Pathsbuild() on SagaBuilder.start() with no step added, which threw IllegalStateExceptionAdd a step before build()#580
SpringAn @HkjHttpClient method whose success type is VoidReplace Void with org.higherkindedj.hkt.Unit#1065
TestingEitherAssert.hasRightNull()Delete the assertion: a Right can no longer hold null#1065
TestingEitherAssert.hasLeftNull()Delete the assertion: a Left can no longer hold null#1077
TestingIOAssert.hasValueNull(), hasNullValue() on VTaskAssert, LazyAssert, StateAssert and WriterAssert, and ReaderAssert's producesNull()Delete the assertion: a lazy computation can no longer give null#1078
TestingUnder a nullness checker, getException() or getValue() on IOAssert or VTaskAssert assigned to, or returned as, a non-null typeDeclare the result @Nullable and check it, or assert on the outcome instead, as hasValue and throwsException do#1094

Deprecated for removal in 0.6.0

The MigrateDeprecationsTo0_6_0 recipe runs each recipe the table names. See 0.6.0 deprecation migration.

DeprecatedReplacementRecipe
A Focus path's each(), on FocusPath, AffinePath, TraversalPath and a navigatoreach(EachInstances.listEach())ReplaceUntypedPathShortcutsRecipe
A Focus path's some()some(FocusPaths.optionalSome())ReplaceUntypedPathShortcutsRecipe
A Focus path's at(index)via(FocusPaths.listAt(index))ReplaceUntypedPathShortcutsRecipe
A Focus path's atKey(key)via(FocusPaths.mapAt(key))ReplaceUntypedPathShortcutsRecipe
A Focus path's traverseOver(traverse)each(EachInstances.fromTraverse(traverse))ReplaceUntypedPathShortcutsRecipe
A Focus path's head(), last(), tail() and init()via(FocusPaths.listHead()), via(FocusPaths.listLast()), via(FocusPaths.listTail()) and via(FocusPaths.listInit())ReplaceUntypedPathShortcutsRecipe
A Focus path's cons() and headTail()via(FocusPaths.listCons()) and via(FocusPaths.listHeadTail())ReplaceUntypedPathShortcutsRecipe
A Focus path's snoc() and initLast()via(FocusPaths.listSnoc()) and via(FocusPaths.listInitLast())ReplaceUntypedPathShortcutsRecipe
ScopeJoiner.resultEither()joinEither() on the Scope, in place of join(); for a StructuredTaskScope you open yourself, the value its join() returns, or the cause of the FailedException it throwsBy hand

Previous: Unreleased: 0.5.1 Next: v0.4.11