Record Class Accumulator<E,A>

java.lang.Object
java.lang.Record
dmx.fun.Accumulator<E,A>
Type Parameters:
E - the accumulation type (log entries, metrics, etc.)
A - the value type
Record Components:
value - the computed value; null only for accumulators created by tell(Object)
accumulated - the side-channel accumulation; never null

@NullMarked public record Accumulator<E,A>(@Nullable A value, E accumulated) extends Record
An immutable pair of a computed value 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 for Validated errors; merged with NonEmptyList.concat(NonEmptyList).
  • int / long — counters; merged with addition.
  • Custom event types — domain audit entries; merged with NonEmptyList or List append.

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 Details

    • Accumulator

      public Accumulator(@Nullable A value, E accumulated)
      Compact canonical constructor — validates that accumulated is non-null.
      Throws:
      NullPointerException - if accumulated is null
  • Method Details

    • of

      public static <E,A> Accumulator<E,A> of(A value, E accumulated)
      Creates an Accumulator pairing 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 type
      A - the value type
      Parameters:
      value - the computed value; must not be null
      accumulated - the initial accumulation; must not be null
      Returns:
      a new Accumulator<E, A>
      Throws:
      NullPointerException - if either argument is null
    • pure

      public static <E,A> Accumulator<E,A> pure(A value, E empty)
      Creates an Accumulator with 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 empty value because Java does not have type classes; common choices are List.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 type
      A - the value type
      Parameters:
      value - the value; must not be null
      empty - the identity / empty accumulation; must not be null
      Returns:
      a new Accumulator<E, A> with empty accumulation
      Throws:
      NullPointerException - if either argument is null
    • tell

      public static <E> Accumulator<E, @Nullable Void> tell(E accumulated)
      Creates an Accumulator that records something without producing a meaningful value.

      The resulting accumulator's value() is null (type Void). Use it at the start of a chain or inside a flatMap(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 a tell result throws NullPointerException because value() is null. Chain with flatMap(Function, BinaryOperator) to produce a real value first.

      Type Parameters:
      E - the accumulation type
      Parameters:
      accumulated - the entry to record; must not be null
      Returns:
      a new Accumulator<E, Void> with null value
      Throws:
      NullPointerException - if accumulated is null
    • hasValue

      public boolean hasValue()
      Returns true if this accumulator was created with of(Object, Object) or pure(Object, Object) and therefore carries a non-null value.

      Returns false only for accumulators created by tell(Object).

      Returns:
      true when value() is non-null
    • map

      public <B> Accumulator<E,B> map(Function<? super A, ? extends B> f)
      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 throws NullPointerException because the value is null. Use flatMap(Function, BinaryOperator) to produce a real value from a tell accumulator.

      Type Parameters:
      B - the type of the new value
      Parameters:
      f - the transformation function; must not be null
      Returns:
      a new Accumulator<E, B> with the transformed value and unchanged accumulation
      Throws:
      NullPointerException - if f is null, or if this accumulator was created by tell(Object) (its value is null)
    • flatMap

      public <B> Accumulator<E,B> flatMap(Function<? super A, ? extends Accumulator<E,B>> f, BinaryOperator<E> merge)
      Chains a computation that itself produces an Accumulator, merging both accumulations using merge.

      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 next Accumulator; must not be null and must not return null
      merge - function that combines the current accumulation with the next step's accumulation; must not be null
      Returns:
      a new Accumulator<E, B> with the chained value and merged accumulation
      Throws:
      NullPointerException - if f or merge is null, or if f returns null
    • mapAccumulated

      public <F> Accumulator<F,A> mapAccumulated(Function<? super E, ? extends F> f)
      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 be null and must not return null
      Returns:
      a new Accumulator<F, A> with the transformed accumulation and unchanged value
      Throws:
      NullPointerException - if f is null or returns null
    • 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 with other by merging their accumulations and applying f to 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 of other
      C - the combined value type
      Parameters:
      other - the second accumulator; must not be null and must not have been created by tell(Object) (its value must be non-null)
      merge - function that combines the two accumulations; must not be null
      f - function that combines the two values; must not be null
      Returns:
      a new Accumulator<E, C> with the combined value and merged accumulation
      Throws:
      NullPointerException - if any argument is null, or if either accumulator was created by tell(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 single Accumulator<E, List<A>>, merging all accumulations left-to-right using merge, starting from empty.

      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 type
      A - the value type of each accumulator
      Parameters:
      accumulators - the list of accumulators to fold; must not be null and no element may be null or created by tell(Object)
      merge - function that combines two accumulations; must not be null
      empty - the identity accumulation used as the starting value; must not be null
      Returns:
      a single Accumulator<E, List<A>> containing all values and the merged log
      Throws:
      NullPointerException - if any argument is null, if any element of accumulators is null, or if any accumulator in the list was created by tell(Object)
    • liftOption

      public static <E,A> Accumulator<E, Option<A>> liftOption(Option<A> option, Function<? super A, ? extends E> someLog, E noneLog)
      Lifts an Option value into an Accumulator, 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 type
      A - the value type inside the option
      Parameters:
      option - the option to lift; must not be null
      someLog - function that produces the accumulation entry when the option is present; must not be null and must not return null
      noneLog - the accumulation entry recorded when the option is absent; must not be null
      Returns:
      an Accumulator<E, Option<A>> pairing the option value with its log entry
      Throws:
      NullPointerException - if any argument is null or if someLog returns null
    • 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 a Try value into an Accumulator, recording a log entry via successLog on success or failureLog on 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 type
      A - the success value type of the Try
      Parameters:
      result - the Try to lift; must not be null
      successLog - function that produces the log entry on success; must not be null and must not return null
      failureLog - function that produces the log entry on failure; must not be null and must not return null
      Returns:
      an Accumulator<E, Try<A>> pairing the Try result with its log entry
      Throws:
      NullPointerException - if any argument is null or if either log function returns null
    • toOption

      public Option<A> toOption()
      Returns the value wrapped in Option.some(Object), or Option.none() if this accumulator was created by tell(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) when hasValue() is true, or None for tell(Object) results
    • toTuple2

      public Tuple2<E, @Nullable A> toTuple2()
      Converts this accumulator to a Tuple2 of (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 _1 and value as _2
    • toResult

      public Result<A,E> toResult()
      Converts this accumulator to a Result: Result.ok(Object) when hasValue() is true, or Result.err(Object) carrying the accumulated side-channel when this accumulator was created by tell(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) when hasValue(), or Err(accumulated) otherwise
    • toString

      public final String toString()
      Returns a string representation of this record class. The representation contains the name of the class, followed by the name and value of each of the record components.
      Specified by:
      toString in class Record
      Returns:
      a string representation of this object
    • hashCode

      public final int hashCode()
      Returns a hash code value for this object. The value is derived from the hash code of each of the record components.
      Specified by:
      hashCode in class Record
      Returns:
      a hash code value for this object
    • equals

      public final boolean equals(Object o)
      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 with Objects::equals(Object,Object).
      Specified by:
      equals in class Record
      Parameters:
      o - the object with which to compare
      Returns:
      true if this object is the same as the o argument; false otherwise.
    • value

      public @Nullable A value()
      Returns the value of the value record component.
      Returns:
      the value of the value record component
    • accumulated

      public E accumulated()
      Returns the value of the accumulated record component.
      Returns:
      the value of the accumulated record component