Combining Timelines with combineLatestWith
As established in the previous chapter, our goal is to implement the “Latest Value Combination” concept. The primary tool for this purpose is the combineLatestWith
combinator. It is the Timeline
library’s implementation of an Applicative operation, specifically designed for combining asynchronous, independent sources.
The Name: combineLatestWith
Section titled “The Name: combineLatestWith”The name combineLatestWith
is chosen deliberately. It is standard terminology in many popular reactive programming libraries (like RxJS) and it perfectly describes the function’s behavior: it combines the latest known values from its sources, applying a user-provided function with them.
Signature
Section titled “Signature”The type signature for combineLatestWith
conforms to the standard map2
pattern for Applicative Functors that we established in Unit 4.
// Located in the TL moduleval combineLatestWith<'a, 'b, 'c> : ('a -> 'b -> 'c) -> Timeline<'a> -> Timeline<'b> -> Timeline<'c>
Let’s break this down:
('a -> 'b -> 'c)
(The Combining Functionf
): A function that takes a value of type'a
as its first argument and a value of type'b
as its second, producing a result of type'c'
.Timeline<'a>
(The First Source): The first source timeline. Its latest value will be passed as the first argument tof
.Timeline<'b>
(The Second Source): The second source timeline. Its latest value will be passed as the second argument tof
.Timeline<'c>
(The Result): A new timeline that will emit the results of thef
function.
The standard way to call this function is: TL.combineLatestWith f timelineA timelineB
.
Behavior: Initialization and Updates
Section titled “Behavior: Initialization and Updates”combineLatestWith
adheres strictly to the philosophy of handling absence established in Section 1. Its core behavior is defined as follows:
-
Initialization: When the result timeline is created,
combineLatestWith
immediately inspects the current values of both source timelines.- If both initial values are non-
null
, it calls the combining functionf
and initializes the result timeline with the output. - If either initial value is
null
,f
is not called. The result timeline is initialized withnull
(or the type’s default value, e.g.,Unchecked.defaultof<'c>
).
- If both initial values are non-
-
Updates: The result timeline listens for updates on both sources. Whenever an update occurs on either
timelineA
ortimelineB
:- It retrieves the latest current values from both sources.
- It performs the same check: if both latest values are non-
null
, it callsf
and defines the output on the result timeline. - If either of the latest values is
null
, it definesnull
(or the default) on the result timeline without callingf
.
Example: Summing Two Number Timelines
Section titled “Example: Summing Two Number Timelines”Let’s see combineLatestWith
in action by creating a timeline that reactively represents the sum of two independent counters.
// Assume Timeline factory, Now, TL module, and isNull helper are accessible
// 1. Create source timelineslet counterA = Timeline 10let counterB = Timeline 20
// 2. Create the sum timeline using the standard function call// The combining function is the standard addition operator (+)let sumTimeline = TL.combineLatestWith (+) counterA counterB
printfn "Initial Sum: %d" (sumTimeline |> TL.at Now)// Expected Output: Initial Sum: 30
// 3. Update counterAprintfn "Updating CounterA to 15..."counterA |> TL.define Now 15printfn "Current Sum after A updated: %d" (sumTimeline |> TL.at Now)// Expected Output: Current Sum after A updated: 35
// 4. Introduce a null valuelet textA : Timeline<string> = Timeline "Hello"let textB : Timeline<string> = Timeline "World"
let combinedText = TL.combineLatestWith (sprintf "%s, %s!") textA textB
printfn "Initial Combined Text: %s" (combinedText |> TL.at Now)// Expected Output: Initial Combined Text: Hello, World!
printfn "Setting textB to null..."textB |> TL.define Now null// The `isNull` check inside nCombineLatestWith prevents the sprintf function from being calledprintfn "Combined Text after B is null: %A" (combinedText |> TL.at Now)// Expected Output: Combined Text after B is null: null
This example demonstrates how combineLatestWith
produces a new, derived state from its sources. It is the foundational building block for all the binary combinations we will explore next.