Record Class Accumulator<E,A>
- Type Parameters:
E- the accumulation type (log entries, metrics, etc.)A- the value type- Record Components:
value- the computed value;nullonly for accumulators created bytell(Object)accumulated- the side-channel accumulation; nevernull
A and a side-channel accumulation E.
Accumulator<E, A> is the functional alternative to mutable global state for
cross-cutting concerns such as logging, metrics, audit trails, and diagnostics. It threads
a growing accumulation value through a computation chain without passing it explicitly as
an argument or storing it in a shared mutable field.
The key invariant: accumulation always continues. Unlike
Result or Try, there is no failure path — every step contributes to both
the value and the accumulation. This makes Accumulator the natural choice when you
want to record a trace of what happened, not just whether it succeeded.
Quick start
// ---- Build a traced computation ----
Accumulator<List<String>, Integer> step1 = Accumulator.of(10, List.of("loaded base value"));
// flatMap chains steps and merges their logs
BinaryOperator<List<String>> concat = (a, b) -> {
var merged = new ArrayList<>(a);
merged.addAll(b);
return merged;
};
Accumulator<List<String>, Integer> result = step1
.flatMap(v -> Accumulator.of(v * 2, List.of("doubled")), concat)
.flatMap(v -> Accumulator.of(v + 5, List.of("added 5")), concat);
result.value(); // 25
result.accumulated(); // ["loaded base value", "doubled", "added 5"]
Typical accumulation types
List<String>— ordered log entries; merged with list concatenation.NonEmptyList<String>— same guarantee as forValidatederrors; merged withNonEmptyList.concat(NonEmptyList).int/long— counters; merged with addition.- Custom event types — domain audit entries; merged with
NonEmptyListorListappend.
Accessing the value
value() returns @Nullable A. For accumulators created via
of(Object, Object) or pure(Object, Object), the value is always
non-null. Accumulators created via tell(Object) carry a null value
(the accumulation side-channel is the only meaningful payload). Call
hasValue() before calling value() if the source is not known.
-
Constructor Summary
ConstructorsConstructorDescriptionAccumulator(@Nullable A value, E accumulated) Compact canonical constructor — validates thataccumulatedis non-null. -
Method Summary
Modifier and TypeMethodDescriptionReturns the value of theaccumulatedrecord component.<B,C> Accumulator <E, C> combine(Accumulator<E, B> other, BinaryOperator<E> merge, BiFunction<? super A, ? super B, ? extends C> f) Combines this accumulator withotherby merging their accumulations and applyingfto both values to produce a new value.final booleanIndicates whether some other object is "equal to" this one.<B> Accumulator<E, B> flatMap(Function<? super A, ? extends Accumulator<E, B>> f, BinaryOperator<E> merge) Chains a computation that itself produces anAccumulator, merging both accumulations usingmerge.final inthashCode()Returns a hash code value for this object.booleanhasValue()Returnstrueif this accumulator was created withof(Object, Object)orpure(Object, Object)and therefore carries a non-null value.static <E,A> Accumulator <E, Option<A>> liftOption(Option<A> option, Function<? super A, ? extends E> someLog, E noneLog) Lifts anOptionvalue into anAccumulator, recording a log entry for both the present and absent cases.static <E,A> Accumulator <E, Try<A>> liftTry(Try<A> result, Function<? super A, ? extends E> successLog, Function<? super Throwable, ? extends E> failureLog) Lifts aTryvalue into anAccumulator, recording a log entry viasuccessLogon success orfailureLogon failure.<B> Accumulator<E, B> Transforms the value, leaving the accumulation unchanged.<F> Accumulator<F, A> mapAccumulated(Function<? super E, ? extends F> f) Transforms the accumulation, leaving the value unchanged.static <E,A> Accumulator <E, A> of(A value, E accumulated) Creates anAccumulatorpairing a computed value with an initial accumulation.static <E,A> Accumulator <E, A> pure(A value, E empty) Creates anAccumulatorwith a value and an empty (identity) accumulation.static <E,A> Accumulator <E, List<A>> sequence(List<? extends Accumulator<E, A>> accumulators, BinaryOperator<E> merge, E empty) Folds a list of accumulators into a singleAccumulator<E, List<A>>, merging all accumulations left-to-right usingmerge, starting fromempty.static <E> Accumulator<E, @Nullable Void> tell(E accumulated) Creates anAccumulatorthat records something without producing a meaningful value.toOption()Returns the value wrapped inOption.some(Object), orOption.none()if this accumulator was created bytell(Object)and has no value.toResult()Converts this accumulator to aResult:Result.ok(Object)whenhasValue()istrue, orResult.err(Object)carrying the accumulated side-channel when this accumulator was created bytell(Object).final StringtoString()Returns a string representation of this record class.toTuple2()Converts this accumulator to aTuple2of(accumulated, value).@Nullable Avalue()Returns the value of thevaluerecord component.
-
Constructor Details
-
Accumulator
Compact canonical constructor — validates thataccumulatedis non-null.- Throws:
NullPointerException- ifaccumulatedisnull
-
-
Method Details
-
of
Creates anAccumulatorpairing a computed value with an initial accumulation.Example:
Accumulator<List<String>, Integer> acc = Accumulator.of(42, List.of("computed answer")); acc.value(); // 42 acc.accumulated(); // ["computed answer"]- Type Parameters:
E- the accumulation typeA- the value type- Parameters:
value- the computed value; must not benullaccumulated- the initial accumulation; must not benull- Returns:
- a new
Accumulator<E, A> - Throws:
NullPointerException- if either argument isnull
-
pure
Creates anAccumulatorwith a value and an empty (identity) accumulation.Use this as the starting point of a chain when the first step has no entries to record. The caller provides the
emptyvalue because Java does not have type classes; common choices areList.of(),0, or an empty string.Example:
Accumulator<List<String>, Integer> start = Accumulator.pure(42, List.of()); // start has value 42 and an empty log- Type Parameters:
E- the accumulation typeA- the value type- Parameters:
value- the value; must not benullempty- the identity / empty accumulation; must not benull- Returns:
- a new
Accumulator<E, A>with empty accumulation - Throws:
NullPointerException- if either argument isnull
-
tell
Creates anAccumulatorthat records something without producing a meaningful value.The resulting accumulator's
value()isnull(typeVoid). Use it at the start of a chain or inside aflatMap(Function, BinaryOperator)step that ignores the incoming value and contributes only to the accumulation:BinaryOperator<List<String>> concat = (a, b) -> { var merged = new ArrayList<>(a); merged.addAll(b); return merged; }; Accumulator<List<String>, Integer> result = Accumulator.tell(List.of("pre-check passed")) .flatMap(__ -> Accumulator.of(42, List.of("computed")), concat); result.value(); // 42 result.accumulated(); // ["pre-check passed", "computed"]Note: calling
map(Function)on atellresult throwsNullPointerExceptionbecausevalue()isnull. Chain withflatMap(Function, BinaryOperator)to produce a real value first.- Type Parameters:
E- the accumulation type- Parameters:
accumulated- the entry to record; must not benull- Returns:
- a new
Accumulator<E, Void>withnullvalue - Throws:
NullPointerException- ifaccumulatedisnull
-
hasValue
public boolean hasValue()Returnstrueif this accumulator was created withof(Object, Object)orpure(Object, Object)and therefore carries a non-null value.Returns
falseonly for accumulators created bytell(Object).- Returns:
truewhenvalue()is non-null
-
map
Transforms the value, leaving the accumulation unchanged.Example:
Accumulator<List<String>, Integer> acc = Accumulator.of(42, List.of("step 1")); Accumulator<List<String>, String> str = acc.map(Object::toString); // ("42", ["step 1"])Note: calling this method on a
tell(Object)result throwsNullPointerExceptionbecause the value isnull. UseflatMap(Function, BinaryOperator)to produce a real value from atellaccumulator.- Type Parameters:
B- the type of the new value- Parameters:
f- the transformation function; must not benull- Returns:
- a new
Accumulator<E, B>with the transformed value and unchanged accumulation - Throws:
NullPointerException- iffisnull, or if this accumulator was created bytell(Object)(its value isnull)
-
flatMap
public <B> Accumulator<E,B> flatMap(Function<? super A, ? extends Accumulator<E, B>> f, BinaryOperator<E> merge) Chains a computation that itself produces anAccumulator, merging both accumulations usingmerge.This is the primary composition combinator: it is the functional equivalent of "run step 1, append its log to the current log, then run step 2 with its result."
Example:
BinaryOperator<List<String>> concat = (a, b) -> { var merged = new ArrayList<>(a); merged.addAll(b); return merged; }; Accumulator<List<String>, Integer> result = Accumulator.of(10, List.of("step 1")) .flatMap(v -> Accumulator.of(v * 2, List.of("step 2")), concat) .flatMap(v -> Accumulator.of(v + 5, List.of("step 3")), concat); result.value(); // 25 result.accumulated(); // ["step 1", "step 2", "step 3"]- Type Parameters:
B- the value type produced by the next step- Parameters:
f- function from the current value to the nextAccumulator; must not benulland must not returnnullmerge- function that combines the current accumulation with the next step's accumulation; must not benull- Returns:
- a new
Accumulator<E, B>with the chained value and merged accumulation - Throws:
NullPointerException- ifformergeisnull, or iffreturnsnull
-
mapAccumulated
Transforms the accumulation, leaving the value unchanged.Use this to convert between accumulation types — for example, to count log entries or to transform raw strings into structured domain objects:
Accumulator<List<String>, Integer> raw = Accumulator.of(42, List.of("event 1")); Accumulator<Integer, Integer> count = raw.mapAccumulated(List::size); // count.value() == 42 // count.accumulated() == 1- Type Parameters:
F- the new accumulation type- Parameters:
f- the transformation function; must not benulland must not returnnull- Returns:
- a new
Accumulator<F, A>with the transformed accumulation and unchanged value - Throws:
NullPointerException- iffisnullor returnsnull
-
combine
public <B,C> Accumulator<E,C> combine(Accumulator<E, B> other, BinaryOperator<E> merge, BiFunction<? super A, ? super B, ? extends C> f) Combines this accumulator withotherby merging their accumulations and applyingfto both values to produce a new value.Both accumulators are evaluated independently — unlike
flatMap(Function, BinaryOperator), there is no sequential dependency between them. Use this when two parallel computations each produce an accumulator and you want to combine their results and logs in one step:BinaryOperator<List<String>> concat = ...; Accumulator<List<String>, User> userAcc = fetchUser(userId); Accumulator<List<String>, Order> orderAcc = fetchOrder(orderId); Accumulator<List<String>, Dashboard> dash = userAcc.combine(orderAcc, concat, Dashboard::new); // dash.accumulated() contains entries from both userAcc and orderAcc- Type Parameters:
B- the value type ofotherC- the combined value type- Parameters:
other- the second accumulator; must not benulland must not have been created bytell(Object)(its value must be non-null)merge- function that combines the two accumulations; must not benullf- function that combines the two values; must not benull- Returns:
- a new
Accumulator<E, C>with the combined value and merged accumulation - Throws:
NullPointerException- if any argument isnull, or if either accumulator was created bytell(Object)
-
sequence
public static <E,A> Accumulator<E,List<A>> sequence(List<? extends Accumulator<E, A>> accumulators, BinaryOperator<E> merge, E empty) Folds a list of accumulators into a singleAccumulator<E, List<A>>, merging all accumulations left-to-right usingmerge, starting fromempty.Example:
BinaryOperator<List<String>> concat = ...; List<Accumulator<List<String>, Integer>> steps = List.of( Accumulator.of(1, List.of("step A")), Accumulator.of(2, List.of("step B")), Accumulator.of(3, List.of("step C")) ); Accumulator<List<String>, List<Integer>> result = Accumulator.sequence(steps, concat, List.of()); result.value(); // [1, 2, 3] result.accumulated(); // ["step A", "step B", "step C"]- Type Parameters:
E- the accumulation typeA- the value type of each accumulator- Parameters:
accumulators- the list of accumulators to fold; must not benulland no element may benullor created bytell(Object)merge- function that combines two accumulations; must not benullempty- the identity accumulation used as the starting value; must not benull- Returns:
- a single
Accumulator<E, List<A>>containing all values and the merged log - Throws:
NullPointerException- if any argument isnull, if any element ofaccumulatorsisnull, or if any accumulator in the list was created bytell(Object)
-
liftOption
public static <E,A> Accumulator<E, Option<A>> liftOption(Option<A> option, Function<? super A, ? extends E> someLog, E noneLog) Lifts anOptionvalue into anAccumulator, recording a log entry for both the present and absent cases.Use this to thread option-returning operations through a traced computation chain while keeping a record of whether each lookup succeeded:
Accumulator<List<String>, Option<User>> acc = Accumulator.liftOption( userRepo.findById(id), user -> List.of("user found: " + user.name()), List.of("user not found for id: " + id) ); // if found: (Some(user), ["user found: Alice"]) // if not found: (None, ["user not found for id: 42"])- Type Parameters:
E- the accumulation typeA- the value type inside the option- Parameters:
option- the option to lift; must not benullsomeLog- function that produces the accumulation entry when the option is present; must not benulland must not returnnullnoneLog- the accumulation entry recorded when the option is absent; must not benull- Returns:
- an
Accumulator<E, Option<A>>pairing the option value with its log entry - Throws:
NullPointerException- if any argument isnullor ifsomeLogreturnsnull
-
liftTry
public static <E,A> Accumulator<E,Try<A>> liftTry(Try<A> result, Function<? super A, ? extends E> successLog, Function<? super Throwable, ? extends E> failureLog) Lifts aTryvalue into anAccumulator, recording a log entry viasuccessLogon success orfailureLogon failure.The
Try<A>itself becomes the value of the returned accumulator, preserving full access to the outcome. The log records what happened regardless of which branch was taken:Accumulator<List<String>, Try<Config>> acc = Accumulator.liftTry( Try.of(() -> ConfigLoader.load(path)), cfg -> List.of("config loaded from " + path), ex -> List.of("config load failed: " + ex.getMessage()) ); acc.accumulated(); // always set — success or failure acc.value(); // Try<Config> — caller decides how to handle it- Type Parameters:
E- the accumulation typeA- the success value type of the Try- Parameters:
result- the Try to lift; must not benullsuccessLog- function that produces the log entry on success; must not benulland must not returnnullfailureLog- function that produces the log entry on failure; must not benulland must not returnnull- Returns:
- an
Accumulator<E, Try<A>>pairing the Try result with its log entry - Throws:
NullPointerException- if any argument isnullor if either log function returnsnull
-
toOption
Returns the value wrapped inOption.some(Object), orOption.none()if this accumulator was created bytell(Object)and has no value.The accumulation is discarded. Use this when only the presence or absence of a value matters, not the log:
Accumulator<List<String>, Integer> acc = Accumulator.of(42, List.of("step 1")); acc.toOption(); // Some(42) Accumulator<List<String>, Void> tell = Accumulator.tell(List.of("entry")); tell.toOption(); // None- Returns:
Some(value)whenhasValue()istrue, orNonefortell(Object)results
-
toTuple2
Converts this accumulator to aTuple2of(accumulated, value).The accumulation occupies the first position (
_1) and the value occupies the second (_2), following the Writer-monad unpacking convention.Accumulator<List<String>, Integer> acc = Accumulator.of(42, List.of("step 1")); Tuple2<List<String>, Integer> pair = acc.toTuple2(); pair._1(); // ["step 1"] pair._2(); // 42- Returns:
- a
Tuple2<E, A>with accumulated as_1and value as_2
-
toResult
Converts this accumulator to aResult:Result.ok(Object)whenhasValue()istrue, orResult.err(Object)carrying the accumulated side-channel when this accumulator was created bytell(Object).Use this at the boundary between a traced computation chain and error-handling code:
Accumulator<List<String>, Config> acc = loadAndTrace(path); Result<Config, List<String>> result = acc.toResult(); // Ok(config) — when a value was computed // Err(["entry..."]) — when only tell() steps ran (no real value was produced)- Returns:
Ok(value)whenhasValue(), orErr(accumulated)otherwise
-
toString
-
hashCode
-
equals
Indicates whether some other object is "equal to" this one. The objects are equal if the other object is of the same class and if all the record components are equal. All components in this record class are compared withObjects::equals(Object,Object). -
value
-
accumulated
Returns the value of theaccumulatedrecord component.- Returns:
- the value of the
accumulatedrecord component
-