コンテンツにスキップ

Chapter 3: Aggregating Lists of Timelines

In the previous chapter, we established TL.And and TL.Or as robust, Monoid-based combinators for merging the state of two Timeline<bool> instances. However, many real-world scenarios involve checking conditions across an entire collection of timelines.

This chapter extends our logic to handle lists of boolean timelines. We will demonstrate how the Monoidal nature of our combinators allows for the elegant aggregation of multiple timelines using the standard List.fold operation, leading us to create the n-ary (multi-input) combinators TL.all and TL.any.

The key to elegantly aggregating a list of values lies in the fold operation combined with a Monoid. Recall that (Timeline<bool>, TL.And, TL.TrueTimeline) and (Timeline<bool>, TL.Or, TL.FalseTimeline) are both Monoids. This is powerful because List.fold is designed to work perfectly with Monoidal structures. It takes a binary combining function, an initial value (the Monoid’s identity element), and a list, then iteratively applies the function.

Because our TL.And and TL.Or operations are associative and have identity elements, we can use List.fold to reliably “roll up” a list of timelines into a single result.

Our goal is to take a list<Timeline<bool>> and produce a single Timeline<bool> that is true only if all timelines in the input list are currently true.

We can define TL.all by folding over the input list using TL.And as the combining operation and TL.TrueTimeline as the initial value.

module TL =
// Assumes TL.And and TL.TrueTimeline have been defined as:
// val And: Timeline<bool> -> Timeline<bool> -> Timeline<bool>
// val TrueTimeline: Timeline<bool>
/// Given a list of boolean timelines, creates a new timeline that is true
/// if all timelines in the input list are currently true.
/// If the input list is empty, it correctly returns TL.TrueTimeline (vacuously true).
let all (booleanTimelines: list<Timeline<bool>>) : Timeline<bool> =
List.fold TL.And TrueTimeline booleanTimelines

When TL.all [t1; t2; t3] is called, it constructs a reactive dependency graph equivalent to TL.And (TL.And TrueTimeline t1) (TL.And t2 t3). A change to any of the input timelines will propagate through the chain of TL.And operations and update the final result.

Symmetrically, our goal for TL.any is to produce a Timeline<bool> that is true if any timeline in the input list is currently true.

We define this by folding with TL.Or as the operation and TL.FalseTimeline as the initial value.

module TL =
// Assumes TL.Or and TL.FalseTimeline have been defined as:
// val Or: Timeline<bool> -> Timeline<bool> -> Timeline<bool>
// val FalseTimeline: Timeline<bool>
/// Given a list of boolean timelines, creates a new timeline that is true
/// if any timeline in the input list is currently true.
/// If the input list is empty, it correctly returns TL.FalseTimeline.
let any (booleanTimelines: list<Timeline<bool>>) : Timeline<bool> =
List.fold TL.Or FalseTimeline booleanTimelines

This builds a reactive graph where the final result is true if any of the source timelines becomes true.

These n-ary combinators allow for clean and declarative aggregation of conditions.

// Assume validationChecks is a list<Timeline<bool>>
let allChecksPassed = TL.all validationChecks
// Assume sensorAlarms is a list<Timeline<bool>>
let systemAlarm = TL.any sensorAlarms
// As always, for efficiency, compose with distinctUntilChanged
let efficientSystemAlarm = systemAlarm |> TL.distinctUntilChanged

With TL.all and TL.any, we now have a complete and robust toolkit for performing logical combinations on any number of boolean timelines. The next step is to apply this toolkit to more complex, real-world data.