Series overview
Part 13 of 2065% complete
2026-04-19•20 min read

Folds, traversals, and sequencing

A fold is the abstraction underneath Stream.reduce: walk a collection, accumulate a result. The algebra from Part 12 supplies the vocabulary — a fold that combines with a monoid needs no seed value, and a fold whose combine is associative can be safely regrouped or parallelized.

The Foldable utility

Java cannot express “any foldable structure” without higher-kinded types, so Foldable is honestly a utility class over Iterable/List — the abstractions are real even if the typeclass is not:

algebra/Foldable.java
public final class Foldable {
public static <A, B> B foldLeft(Iterable<A> values, B initial, BiFunction<B, A, B> folder) {
B accumulated = initial;
for (A value : values) {
accumulated = folder.apply(accumulated, value);
}
return accumulated;
}
public static <A, M> M foldMap(Iterable<A> values, Monoid<M> monoid,
Fn1<? super A, ? extends M> mapper) {
M accumulated = monoid.empty();
for (A value : values) {
accumulated = monoid.combine(accumulated, mapper.apply(value));
}
return accumulated;
}
public static <T> T combineAll(Iterable<T> values, Monoid<T> monoid) {
return foldMap(values, monoid, Fn1.identity());
}
}

foldMap is the workhorse: map each element into a monoidal type, then combine. “Total income across applications” is foldMap(apps, Monoid.longSum(), ApplicationInput::declaredIncome). foldRight (in the repo) walks from the back — the distinction matters for laziness in Part 14.

Traverse and sequence: turning a list of contexts inside out

The scheme engine collects document-verification results: a List<Result<Document, RegistryError>>. What the caller wants is Result<List<Document>, RegistryError> — all the documents, or the first failure:

data/Results.java
public static <T, E> Result<List<T>, E> sequence(List<Result<T, E>> results) {
List<T> values = new ArrayList<>(results.size());
for (Result<T, E> result : results) {
switch (result) {
case Result.Success<T, E>(var value) -> values.add(value);
case Result.Failure<T, E>(var error) -> {
return Result.failure(error);
}
}
}
return Result.success(values);
}

traverse is sequence composed with a per-element function — apply verify(document) to each element, collecting Results as you go. And when “first failure” is wrong — when the citizen should see every invalid document — Validated.sequence accumulates instead:

data/Validated.java
public static <E, T> Validated<E, List<T>> sequence(
List<Validated<E, T>> values, Semigroup<E> semigroup) {
Validated<E, List<T>> accumulated = valid(new ArrayList<>());
for (Validated<E, T> value : values) {
accumulated = map2(accumulated, value, semigroup, (list, element) -> {
list.add(element);
return list;
});
}
return accumulated.map(List::copyOf);
}

Results.sequence

Validated.sequence

List<Result<Doc, E>>

Result<List<Doc>, E>

first failure wins

List<Validated<E, Doc>>

Validated<E, List<Doc>>

all errors kept

Results.sequence

Validated.sequence

List<Result<Doc, E>>

Result<List<Doc>, E>

first failure wins

List<Validated<E, Doc>>

Validated<E, List<Doc>>

all errors kept

The choice is the Part 8 rule applied to collections: dependent → Result.sequence stops early; independent → Validated.sequence reports everything. Pick per workflow, not per type.

Compared with the JDK: Stream.collect with a Collector is a fold; Collectors.reducing is foldMap for a monoid you write inline. The jfp versions exist to make the structure visible — foldMap names the monoid explicitly, which is the difference between “an accumulator that happens to work” and “an accumulation you can state laws about.”

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind