Chapter 3: foldTimelines — Folding with Monoids
Introduction
Section titled “Introduction”In Chapter 2, we learned how to combine two Timeline
s using binary operations that form a Monoid. In this chapter, we will extend that concept further and explore how to fold a list of Timeline
s (an arbitrary number of Timeline
s) into a single Timeline
.
The general-purpose tool for this is foldTimelines
. This is an application of the standard folding operation in functional programming, fold
(or reduce
), to the world of Timeline
, and it relies completely on the Monoid structure established in Chapter 2.
foldTimelines
Section titled “foldTimelines”F#: foldTimelines: (Timeline<'b> -> Timeline<'a> -> Timeline<'b>) -> Timeline<'b> -> list<Timeline<'a>> -> Timeline<'b>
Section titled “F#: foldTimelines: (Timeline<'b> -> Timeline<'a> -> Timeline<'b>) -> Timeline<'b> -> list<Timeline<'a>> -> Timeline<'b>”TS: foldTimelines<A, B>(timelines: readonly Timeline<A>[], initialTimeline: Timeline<B>, accumulator: (acc: Timeline<B>, current: Timeline<A>) => Timeline<B>): Timeline<B>
Section titled “TS: foldTimelines<A, B>(timelines: readonly Timeline<A>[], initialTimeline: Timeline<B>, accumulator: (acc: Timeline<B>, current: Timeline<A>) => Timeline<B>): Timeline<B>”// TS Implementationexport const foldTimelines = <A, B>( timelines: readonly Timeline<A>[], initialTimeline: Timeline<B>, accumulator: (acc: Timeline<B>, current: Timeline<A>) => Timeline<B>): Timeline<B> => timelines.reduce(accumulator, initialTimeline);
foldTimelines
takes a list of Timeline
s and generates a single Timeline
by successively applying the “binary operation” of a Monoid to the elements of the list, using the “identity element” of the Monoid as the initial value.
The breakdown of the arguments corresponds perfectly to the components of a Monoid:
timelines
: The list ofTimeline
s to be folded.initialTimeline
: The identity element of the Monoid (the initial value for the fold).accumulator
: The binary operation of the Monoid (e.g.,orOf
as defined in Chapter 2).
Derivation of High-Level APIs
Section titled “Derivation of High-Level APIs”The true value of foldTimelines
is that intuitive high-level APIs like anyOf
and sumOf
can be realized simply by combining foldTimelines
with various Monoids, without needing any special implementation. They are merely thin wrappers around foldTimelines
.
F#: anyOf: list<Timeline<bool>> -> Timeline<bool>
Section titled “F#: anyOf: list<Timeline<bool>> -> Timeline<bool>”TS: anyOf(booleanTimelines: readonly Timeline<boolean>[]): Timeline<boolean>
Section titled “TS: anyOf(booleanTimelines: readonly Timeline<boolean>[]): Timeline<boolean>”// TS Implementationexport const anyOf = (booleanTimelines: readonly Timeline<boolean>[]): Timeline<boolean> => { return foldTimelines(booleanTimelines, FalseTimeline, orOf);};
Logical OR across multiple boolean timelines. This is derived by applying foldTimelines
using the Logical OR Monoid:
- Identity Element:
Timeline(false)
- Binary Operation:
orOf
// TS Usage Exampleconst flags = [Timeline(true), Timeline(false), Timeline(false)];const hasAnyTrue = anyOf(flags);console.log(hasAnyTrue.at(Now)); // true
F#: allOf: list<Timeline<bool>> -> Timeline<bool>
Section titled “F#: allOf: list<Timeline<bool>> -> Timeline<bool>”TS: allOf(booleanTimelines: readonly Timeline<boolean>[]): Timeline<boolean>
Section titled “TS: allOf(booleanTimelines: readonly Timeline<boolean>[]): Timeline<boolean>”// TS Implementationexport const allOf = (booleanTimelines: readonly Timeline<boolean>[]): Timeline<boolean> => { return foldTimelines(booleanTimelines, TrueTimeline, andOf);};
Logical AND across multiple boolean timelines. This is derived using the Logical AND Monoid:
- Identity Element:
Timeline(true)
- Binary Operation:
andOf
// TS Usage Exampleconst flags = [Timeline(true), Timeline(true), Timeline(false)];const allTrue = allOf(flags);console.log(allTrue.at(Now)); // false
TS: sumOf(numberTimelines: readonly Timeline<number>[]): Timeline<number>
Section titled “TS: sumOf(numberTimelines: readonly Timeline<number>[]): Timeline<number>”// TS Implementationexport const sumOf = (numberTimelines: readonly Timeline<number>[]): Timeline<number> => { return foldTimelines(numberTimelines, Timeline(0), addOf);};
Sum of multiple number timelines. This is derived using the Addition Monoid:
- Identity Element:
Timeline(0)
- Binary Operation:
addOf
// TS Usage Exampleconst numbers = [Timeline(10), Timeline(20), Timeline(30)];const total = sumOf(numbers);console.log(total.at(Now)); // 60
TS: maxOf(timelines): Timeline<number>
Section titled “TS: maxOf(timelines): Timeline<number>”// TS Implementationexport const maxOf = (numberTimelines: readonly Timeline<number>[]): Timeline<number> => { return foldTimelines(numberTimelines, Timeline(-Infinity), maxOf2);};
Maximum value across multiple number timelines. This is derived from a Maximum Monoid:
- Identity Element:
Timeline(-Infinity)
- Binary Operation: A binary
max
function for timelines.
// TS Usage Exampleconst numbers = [Timeline(10), Timeline(50), Timeline(30)];const maximum = maxOf(numbers);console.log(maximum.at(Now)); // 50
TS: minOf(timelines): Timeline<number>
Section titled “TS: minOf(timelines): Timeline<number>”// TS Implementationexport const minOf = (numberTimelines: readonly Timeline<number>[]): Timeline<number> => { return foldTimelines(numberTimelines, Timeline(Infinity), minOf2);};
Minimum value across multiple number timelines. This is derived from a Minimum Monoid:
- Identity Element:
Timeline(Infinity)
- Binary Operation: A binary
min
function for timelines.
// TS Usage Exampleconst numbers = [Timeline(10), Timeline(50), Timeline(30)];const minimum = minOf(numbers);console.log(minimum.at(Now)); // 10
averageOf
Section titled “averageOf”TS: averageOf(timelines): Timeline<number>
Section titled “TS: averageOf(timelines): Timeline<number>”// TS Implementationexport const averageOf = (numberTimelines: readonly Timeline<number>[]): Timeline<number> => { if (numberTimelines.length === 0) return Timeline(0); return sumOf(numberTimelines).map(sum => sum / numberTimelines.length);};
Average of multiple number timelines. While not a simple monoid, this is conceptually similar. It can be derived by folding to find the sumOf
and the count of timelines, then creating a new timeline that divides them.
// TS Usage Exampleconst numbers = [Timeline(10), Timeline(20), Timeline(30)];const avg = averageOf(numbers);console.log(avg.at(Now)); // 20
listOf
Section titled “listOf”F#: listOf: list<Timeline<'a>> -> Timeline<'a list>
Section titled “F#: listOf: list<Timeline<'a>> -> Timeline<'a list>”TS: listOf<A>(timelines: readonly Timeline<A>[]): Timeline<A[]>
Section titled “TS: listOf<A>(timelines: readonly Timeline<A>[]): Timeline<A[]>”// TS Implementationexport const listOf = <A>( timelines: readonly Timeline<A>[]): Timeline<A[]> => { const emptyArrayTimeline = Timeline<A[]>([]); return foldTimelines(timelines, emptyArrayTimeline, concatOf) as Timeline<A[]>;};
Combines multiple timelines into a single timeline holding an array of their values. This is derived from the Array Monoid emphasized in Chapter 2:
- Identity Element:
Timeline([])
- Binary Operation:
concatOf
Since this function can simply combine multiple Timeline
s into a single Timeline
holding an array of their values, it is easy for an AI to handle when generating code and tends to be used preferentially.
// TS Usage Exampleconst items = [Timeline("a"), Timeline("b"), Timeline("c")];const list = listOf(items);console.log(list.at(Now)); // ["a", "b", "c"]
Conclusion
Section titled “Conclusion”It is now clear that the high-level API group, including anyOf
and sumOf
, is not a collection of ad-hoc, individual functions. They are all derived with mathematical necessity from a single general abstraction, foldTimelines
, and the Monoid structure that each type possesses.
This hierarchical structure of combineLatestWith
(Applicative) → binary operation (Monoid) → foldTimelines
→ high-level API is the core of this library’s design beauty and the robustness that eliminates code duplication.