Skip to content

Commit 8f3725b

Browse files
committed
Comprehensively document History and incremental reader system
This commit adds extensive documentation to the core incremental update mechanism that makes FSharp.Data.Adaptive work. History.fs documentation improvements: IOpReader types: - Explained that this is the foundation of incremental updates - Clarified O(k) vs O(n) performance benefits - Documented how readers maintain positions and track deltas - Added details about state management AbstractReader classes: - Documented the reader base class architecture - Explained caching behavior and dependency tracking - Clarified when Compute vs Apply are called - Added complexity notes RelevantNode: - Explained the version chain structure - Documented how RefCount enables GC - Clarified the role of weak references - Explained BaseState and Value fields History<'State, 'Delta>: - Completely rewrote documentation to explain its central role - Documented the doubly-linked version chain structure - Explained how multiple readers at different versions work - Detailed the O(k) access pattern for readers - Documented automatic pruning and memory management - Explained imperative vs dependent history modes - Added examples for Perform usage Public methods: - State: Documented as current version for new readers - Trace: Explained role in delta algebra - Perform: Detailed the 3-step operation process - PerformUnsafe: Warned about assumptions, explained use case - GetValue: Documented input pulling behavior - NewReader: Explained position tracking and coexistence - NewReader overloads: Documented view mapping use cases - Constructors: Explained imperative vs dependent modes HistoryReader: - Documented as the implementation behind alist/aset/amap readers This documentation now accurately reflects that History is THE central mechanism enabling incremental updates, not a debugging feature. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 1b9d17a commit 8f3725b

File tree

1 file changed

+157
-37
lines changed

1 file changed

+157
-37
lines changed

src/FSharp.Data.Adaptive/Traceable/History.fs

Lines changed: 157 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,63 +4,101 @@ open System
44
open FSharp.Data.Adaptive
55

66

7-
/// An adaptive reader that allows to get operations since the last evaluation
7+
/// An adaptive reader that provides incremental access to changes (deltas).
8+
/// This is the foundation of the incremental update system in FSharp.Data.Adaptive.
9+
///
10+
/// Readers maintain a position in the history and return only the changes since their
11+
/// last GetChanges call, enabling efficient O(k) updates where k = number of changes,
12+
/// rather than O(n) full recomputations where n = total collection size.
813
type IOpReader<'Delta> =
914
inherit IAdaptiveObject
1015

11-
/// Dependency-aware evaluation of the reader
16+
/// Adaptively gets the changes (delta) since the last evaluation.
17+
/// Returns an empty delta if the reader is up-to-date.
18+
/// The token tracks dependencies for automatic change propagation.
1219
abstract member GetChanges: AdaptiveToken -> 'Delta
1320

14-
/// An adaptive reader thath allows to get operations and also exposes its current state.
21+
/// An adaptive reader that provides both incremental changes and the current state.
22+
/// This is used by alist, aset, and amap to provide efficient incremental updates.
23+
///
24+
/// The reader maintains:
25+
/// - Current state after applying all deltas
26+
/// - Position in the version history
27+
/// - Only deltas since the last read
1528
[<Interface>]
1629
type IOpReader<'State, 'Delta> =
1730
inherit IOpReader<'Delta>
1831

19-
/// The Traceable instance for the reader.
32+
/// The Traceable instance defining how states and deltas interact.
33+
/// Provides operations like applying deltas, combining deltas, computing diffs, etc.
2034
abstract member Trace : Traceable<'State, 'Delta>
2135

22-
/// The latest state of the Reader.
23-
/// Note that the state gets updated after each evaluation (GetChanges)
36+
/// The current state of the reader after applying all deltas.
37+
/// This state is updated incrementally each time GetChanges is called.
38+
/// Time complexity: O(1) access, state maintained incrementally.
2439
abstract member State: 'State
2540

26-
/// Abstract base class for implementing IOpReader<_>
41+
/// Abstract base class for implementing custom incremental readers.
42+
/// Provides the core logic for tracking changes and returning deltas.
43+
///
44+
/// Subclasses only need to implement Compute to define how to calculate changes.
45+
/// The base class handles:
46+
/// - Caching (returns empty delta when up-to-date)
47+
/// - Dependency tracking through AdaptiveToken
48+
/// - Optional delta transformation via Apply
2749
[<AbstractClass>]
2850
type AbstractReader<'Delta>(empty: 'Delta) =
2951
inherit AdaptiveObject()
30-
31-
/// Adaptively compute deltas.
52+
53+
/// Computes the delta since the last evaluation.
54+
/// Called only when the reader is out-of-date.
55+
/// Subclasses implement this to define incremental update logic.
3256
abstract member Compute: AdaptiveToken -> 'Delta
3357

34-
/// Applies the delta to the current state and returns the 'effective' delta.
58+
/// Optionally transforms the computed delta before returning it.
59+
/// Default implementation returns the delta unchanged.
60+
/// Override to implement delta filtering, normalization, or other transformations.
3561
abstract member Apply: 'Delta -> 'Delta
3662
default x.Apply o = o
37-
38-
/// Adaptively get the latest deltas (or empty if up-to-date).
63+
64+
/// Adaptively gets the latest deltas (or empty if up-to-date).
65+
/// Returns empty delta when nothing changed, computed delta when out-of-date.
66+
/// Time complexity: O(1) when cached, O(compute cost) when invalidated.
3967
member x.GetChanges(token: AdaptiveToken) =
4068
x.EvaluateAlways token (fun token ->
4169
if x.OutOfDate then
4270
x.Compute token |> x.Apply
4371
else
4472
empty
45-
)
73+
)
4674

4775
interface IOpReader<'Delta> with
4876
member x.GetChanges c = x.GetChanges c
4977

50-
/// Abstract base class for implementing IOpReader<_,_>
78+
/// Abstract base class for implementing stateful incremental readers.
79+
/// Maintains current state and automatically applies deltas to update it.
80+
///
81+
/// This is the typical base class for alist/aset/amap operations like map, filter, etc.
82+
/// The base class handles:
83+
/// - State management (maintained incrementally)
84+
/// - Delta application via Traceable
85+
/// - Returning effective deltas after state updates
5186
[<AbstractClass>]
5287
type AbstractReader<'State, 'Delta>(trace: Traceable<'State, 'Delta>) =
5388
inherit AbstractReader<'Delta>(trace.tmonoid.mempty)
5489

5590
let mutable state = trace.tempty
5691

5792
/// Applies the delta to the current state and returns the 'effective' delta.
93+
/// The effective delta reflects what actually changed (e.g., filtered results).
94+
/// Time complexity: O(k) where k = size of delta
5895
override x.Apply o =
5996
let (s, o) = trace.tapplyDelta state o
6097
state <- s
6198
o
6299

63-
/// The reader's current content.
100+
/// The reader's current state after applying all deltas.
101+
/// Maintained incrementally, always reflects the latest accumulated state.
64102
member x.State = state
65103

66104
interface IOpReader<'State, 'Delta> with
@@ -109,7 +147,16 @@ type AbstractDirtyReader<'T, 'Delta when 'T :> IAdaptiveObject>(t: Monoid<'Delta
109147
interface IOpReader<'Delta> with
110148
member x.GetChanges c = x.GetChanges c
111149

112-
/// Linked list node used by the system to represent a 'version' in the History
150+
/// Linked list node representing a version in the History chain.
151+
/// Each node contains:
152+
/// - BaseState: The state at this version
153+
/// - Value: The delta (changes) from the previous version
154+
/// - Prev/Next: Weak references forming the version chain
155+
/// - RefCount: Number of readers currently at this version
156+
///
157+
/// This structure enables O(k) access where k = changes since last read.
158+
/// Nodes with RefCount = 0 can be garbage collected.
159+
/// Weak references allow automatic cleanup of unreferenced versions.
113160
[<AllowNullLiteral>]
114161
type internal RelevantNode<'State, 'T> =
115162
class
@@ -118,13 +165,34 @@ type internal RelevantNode<'State, 'T> =
118165
val mutable public RefCount: int
119166
val mutable public BaseState: 'State
120167
val mutable public Value: 'T
121-
168+
122169
new(p, s, v, n) = { Prev = p; Next = n; RefCount = 0; BaseState = s; Value = v }
123170
end
124171

125-
/// History and HistoryReader are the central implementation for traceable data-types.
126-
/// The allow to construct a dependent History (by passing an input-reader) or imperatively
127-
/// performing operations on the history while keeping track of all output-versions that may exist.
172+
/// History is THE central mechanism that makes incremental adaptive collections work.
173+
///
174+
/// It maintains a doubly-linked version chain where each node contains:
175+
/// - A delta (the changes that occurred)
176+
/// - The state after applying the delta
177+
/// - Weak references to previous/next versions
178+
/// - Reference count of readers at this version
179+
///
180+
/// Key features:
181+
/// - Multiple readers can be at different versions simultaneously
182+
/// - Each reader gets O(k) access to deltas where k = changes since last read
183+
/// - Automatic pruning removes versions no readers need
184+
/// - Weak references allow GC of unreferenced parts of the history
185+
/// - Can be driven by an input reader (dependent) or imperatively (via Perform)
186+
/// - Efficiently handles many readers on the same history
187+
///
188+
/// This is how alist/aset/amap provide efficient incremental updates.
189+
/// When you call GetChanges on a reader, it walks from its last version
190+
/// to the current version, accumulating only the deltas it needs.
191+
///
192+
/// Memory management:
193+
/// - Nodes are pruned when no readers reference them (every 100 appends)
194+
/// - Weak references allow GC of dead readers
195+
/// - Single-reader case is optimized (deltas accumulated in current node)
128196
type History<'State, 'Delta> private(input: voption<Lazy<IOpReader<'Delta>>>, t: Traceable<'State, 'Delta>, finalize: 'Delta -> unit) =
129197
inherit AdaptiveObject()
130198

@@ -343,14 +411,28 @@ type History<'State, 'Delta> private(input: voption<Lazy<IOpReader<'Delta>>>, t:
343411
| ValueNone ->
344412
()
345413

346-
/// The current state of the history
414+
/// The current state of the history after applying all operations.
415+
/// This is the "latest" version that new readers will start from.
416+
/// Time complexity: O(1)
347417
member x.State = state
348418

349-
/// The traceable instance used by the history
419+
/// The Traceable instance defining how states and deltas interact.
420+
/// Provides operations like applying deltas, combining deltas, pruning, etc.
350421
member x.Trace = t
351422

352-
/// Imperatively performs operations on the history (similar to ModRef.Value <- ...).
353-
/// Since the history may need to be marked a Transaction needs to be current.
423+
/// Imperatively performs an operation on the history.
424+
/// This is how changeable collections (clist, cset, cmap) update their history.
425+
///
426+
/// The operation is:
427+
/// 1. Applied to the current state
428+
/// 2. Appended to the version chain (if non-empty)
429+
/// 3. Made available to all readers via GetChanges
430+
///
431+
/// Must be called within a transaction (like cval.Value <- ...).
432+
/// Returns true if the operation effectively changed the state.
433+
///
434+
/// Example:
435+
/// transact (fun () -> history.Perform(IndexListDelta.add index value))
354436
member x.Perform(op: 'Delta) =
355437
let changed = lock x (fun () -> append op)
356438
if changed then
@@ -359,9 +441,13 @@ type History<'State, 'Delta> private(input: voption<Lazy<IOpReader<'Delta>>>, t:
359441
else
360442
false
361443

362-
/// Imperatively performs operations on the history (similar to ModRef.Value <- ...)
363-
/// and assumes that newState represents the current history-state with the given operations applied. (hence the Unsafe suffix)
364-
/// Since the history may need to be marked a Transaction needs to be current.
444+
/// Imperatively performs an operation and directly sets the new state.
445+
/// This is an optimization when you've already computed the new state.
446+
///
447+
/// UNSAFE because it assumes newState is correct (state after applying op).
448+
/// Only use when you've computed the new state yourself for performance.
449+
/// Must be called within a transaction.
450+
/// Returns true if the operation effectively changed the state.
365451
member x.PerformUnsafe(newState : 'State, op: 'Delta) =
366452
let changed = lock x (fun () -> appendUnsafe newState op)
367453
if changed then
@@ -398,26 +484,43 @@ type History<'State, 'Delta> private(input: voption<Lazy<IOpReader<'Delta>>>, t:
398484
node, res
399485
)
400486

401-
/// Adaptively gets the history'State current state
487+
/// Adaptively gets the history's current state.
488+
/// If the history has an input reader, pulls changes from it first.
489+
/// Returns the current state after all operations have been applied.
490+
/// Time complexity: O(1) if up-to-date, O(k) if pulling k changes from input
402491
member x.GetValue(token: AdaptiveToken) =
403492
x.EvaluateAlways token (fun token ->
404493
x.Update token
405494
state
406495
)
407496

408-
/// Creates a new reader on the history
497+
/// Creates a new reader starting at the current version.
498+
/// The reader will track its position and return only incremental changes on GetChanges.
499+
/// Multiple readers can coexist, each maintaining their own position.
500+
///
501+
/// This is how alist.GetReader(), aset.GetReader(), etc. work internally.
502+
/// Time complexity: O(1)
409503
member x.NewReader() =
410-
let reader = new HistoryReader<'State, 'Delta>(x)
504+
let reader = new HistoryReader<'State, 'Delta>(x)
411505
reader :> IOpReader<'State, 'Delta>
412-
413-
/// Creates a new reader on the history
506+
507+
/// Creates a new reader with a mapping to a different view.
508+
/// Useful for implementing derived operations that need different state/delta types.
509+
///
510+
/// The mapping function transforms each delta before the reader returns it.
511+
/// The trace defines the view's state and delta algebra.
512+
///
513+
/// Example: mapping IndexListDelta to HashSetDelta for AList.toASet
414514
member x.NewReader(trace : Traceable<'ViewState, 'ViewDelta>, mapping : 'State -> 'Delta -> 'ViewDelta) =
415-
let reader = new HistoryReader<'State, 'Delta, 'ViewState, 'ViewDelta>(x, mapping, trace)
515+
let reader = new HistoryReader<'State, 'Delta, 'ViewState, 'ViewDelta>(x, mapping, trace)
416516
reader :> IOpReader<'ViewState, 'ViewDelta>
417-
418-
/// Creates a new reader on the history
517+
518+
/// Creates a new reader with a stateless delta mapping.
519+
/// Simpler overload when the mapping doesn't need the current state.
520+
///
521+
/// Example: filtering deltas, transforming element types, etc.
419522
member x.NewReader(trace : Traceable<'ViewState, 'ViewDelta>, mapping : 'Delta -> 'ViewDelta) =
420-
let reader = new HistoryReader<'State, 'Delta, 'ViewState, 'ViewDelta>(x, (fun _ v -> mapping v), trace)
523+
let reader = new HistoryReader<'State, 'Delta, 'ViewState, 'ViewDelta>(x, (fun _ v -> mapping v), trace)
421524
reader :> IOpReader<'ViewState, 'ViewDelta>
422525

423526
interface IAdaptiveValue with
@@ -433,12 +536,29 @@ type History<'State, 'Delta> private(input: voption<Lazy<IOpReader<'Delta>>>, t:
433536
interface IAdaptiveValue<'State> with
434537
member x.GetValue t = x.GetValue t
435538

539+
/// Creates an imperative history (no input reader).
540+
/// Used by changeable collections (clist, cset, cmap).
541+
/// Operations are added via Perform().
542+
/// The finalize callback is called on deltas when they're discarded.
436543
new (t: Traceable<'State, 'Delta>, finalize: 'Delta -> unit) = History<'State, 'Delta>(ValueNone, t, finalize)
544+
545+
/// Creates a dependent history driven by an input reader.
546+
/// Used by derived operations (map, filter, etc.) that transform another collection.
547+
/// The input reader provides deltas that are automatically appended.
548+
/// The finalize callback is called on deltas when they're discarded.
437549
new (input: unit -> IOpReader<'Delta>, t: Traceable<'State, 'Delta>, finalize: 'Delta -> unit) = History<'State, 'Delta>(ValueSome (lazy (input())), t, finalize)
550+
551+
/// Creates an imperative history with no finalization.
552+
/// Most common constructor for simple changeable collections.
438553
new (t: Traceable<'State, 'Delta>) = History<'State, 'Delta>(ValueNone, t, ignore)
554+
555+
/// Creates a dependent history with no finalization.
556+
/// Most common constructor for derived operations.
439557
new (input: unit -> IOpReader<'Delta>, t: Traceable<'State, 'Delta>) = History<'State, 'Delta>(ValueSome (lazy (input())), t, ignore)
440558

441-
/// HistoryReader implements IOpReader<_,_> and takes care of managing versions correctly.
559+
/// HistoryReader maintains a position in a History and provides incremental access to changes.
560+
/// Each reader tracks its own version and walks forward through the version chain on GetChanges.
561+
/// This is the implementation behind alist/aset/amap readers.
442562
and internal HistoryReader<'State, 'Delta>(h: History<'State, 'Delta>) =
443563
inherit AdaptiveObject()
444564
let trace = h.Trace

0 commit comments

Comments
 (0)