コンテンツにスキップ

Chapter 5: Robustness Through Algebraic Structure

Introduction: A Unified Ecosystem

The preceding chapters established the Timeline<'a> concept, grounded in the Block Universe model (Chapters 0 & 2), introduced the TL.map operation for transformations and the idea of a dependency graph (Chapter 3), and demonstrated how I/O operations can be integrated (Chapter 4). The result is a unified “ecosystem” where all dynamic values and interactions are represented and managed consistently through the Timeline abstraction and its associated operations.

Now that we have this complete Timeline-centric system, we can explore one of its most significant benefits: robustness. This robustness arises because the Timeline type, equipped with operations like TL.map and TL.bind (which we will verify in this chapter, referencing their definitions from Timeline.fs), adheres to well-defined mathematical laws, specifically those associated with algebraic structures like Functors and Monads.

This chapter will verify these properties and explain their connection to robustness:

  1. First, we will confirm that Timeline satisfies the Functor laws using illustrative code examples based on the public API.
  2. Second, we will (re)introduce TL.bind and verify that Timeline satisfies the Monad laws, choosing the formulation based on Kleisli composition (TL.(>>>)), which highlights the underlying Monoid structure.
  3. Third, we will discuss how these verified Functor and Monad (Monoid) properties contribute to the robustness of systems built with Timeline.
  4. Fourth, we will argue why these algebraic properties are, in a sense, an obvious consequence of the underlying immutable dependency graph model introduced earlier.

A Philosophical Prerequisite: As previously established (Chapter 2, Section 2.8), conventional functional programming might dismiss Timeline<'a> as impure due to its internal use of a mutable field (_last). However, our framework views Timeline<'a> as a simulation of observing an immutable Block Universe. The internal mutability of _last is the correct way to model the changing viewpoint (Now) against the immutable data trajectory. Therefore, we consider the Timeline<'a> object, from the perspective of its observable behavior over time (the sequence of values obtainable via TL.at), as a conceptually immutable entity representing the entire time-varying value. It is on this behavioral equivalence and conceptual immutability that we base the following verification. We assert that two Timeline objects are equivalent if they produce the same sequence of values over time for any given sequence of updates to their sources.


5.1 Verifying Functor Laws for Timeline<'a>

Section titled “5.1 Verifying Functor Laws for Timeline<'a>”

A type constructor T is a Functor if it supports a map function (fmap in Haskell, Select in LINQ) with the following signature (adapted for Timeline from Timeline.fs): (*Signature from TL module in Timeline.fs *) val map<'a, 'b> : ('a -> 'b) -> Timeline<'a> -> Timeline<'b> And satisfies two laws. We verify these using illustrative F# code examples written in pipeline style (timelineA |> TL.map f), focusing only on the observable behavior.

Setup for Functor Examples:

// Assume Timeline factory, Now value, and TL module functions (TL.map, TL.define, TL.at) are accessible
// No 'open Timeline' or 'open Timeline.TL'
// Sample simple functions using 'fun' keyword style
let f1 : int -> string =
fun i -> sprintf "Value: %d" i
let f2 : string -> bool =
fun s -> s.Length > 10
// Standard identity function using 'fun' keyword style
let id<'a> : 'a -> 'a = // Generic identity function
fun x -> x
// Initial timeline using the factory function
let timelineIntF : Timeline<int> = Timeline 5 // Explicit type

Law 1: Identity Equation: map id = id Which means: For all t : Timeline<'a>, t |> TL.map id is behaviorally equivalent to t.

  • Illustrative Code:

    // --- LHS ---
    let lhsTimelineF_Id : Timeline<int> = timelineIntF |> TL.map id // Explicit TL.map
    // --- RHS ---
    let rhsTimelineF_Id : Timeline<int> = timelineIntF // Applying id doesn't change it (conceptually)
  • Verification (Conceptual):

    • Initial Values: lhsTimelineF_Id is created by applying id to timelineIntF’s initial value (5), so its initial value observable via TL.at Now is 5. rhsTimelineF_Id’s initial value is 5. Both start with the same observable value.
    • Updates: If we execute timelineIntF |> TL.define Now 15, the TL.map operation ensures that lhsTimelineF_Id subsequently reflects the value id 15 = 15 when queried with TL.at Now. The RHS is timelineIntF, so it also reflects 15. Both timelines exhibit identical value sequences over time.
  • Equivalence Explanation: The t |> TL.map id operation creates a new timeline that starts with the same value as t and mirrors every update because the mapped function id doesn’t change the value. The library’s internal mechanism (DependencyCore) guarantees this propagation. Thus, the LHS behaves identically to the RHS (t). Therefore, the Identity Law holds (behaviorally).

Law 2: Composition Equation: map (f >> g) = map f >> map g (where map f >> map g means fun t -> t |> TL.map f |> TL.map g) Which means: For all f, g, t, t |> TL.map (f >> g) is behaviorally equivalent to t |> TL.map f |> TL.map g. (Here >> denotes standard function composition: apply f then g).

  • Illustrative Code:

    // f1 : int -> string
    // f2 : string -> bool
    // composedFunc : int -> bool
    let composedFunc : int -> bool = f1 >> f2 // Standard function composition
    // --- LHS ---
    let lhsTimelineF_Comp : Timeline<bool> = timelineIntF |> TL.map composedFunc // Explicit TL.map
    // --- RHS ---
    // Apply the mapping functions sequentially using pipeline
    let rhsTimelineF_Comp : Timeline<bool> = timelineIntF |> TL.map f1 |> TL.map f2 // Explicit TL.map
  • Verification (Conceptual):

    • Initial Values: LHS applies composedFunc (which is f1 >> f2) to 5. f1 5 evaluates to “Value: 5”, then f2 "Value: 5" evaluates to false (since length 9 !> 10). So, lhsTimelineF_Comp initial value is false. RHS first applies TL.map f1 to timelineIntF (initial value 5), resulting in an intermediate Timeline<string> with initial value “Value: 5”. Then, TL.map f2 is applied to this intermediate timeline, yielding a final rhsTimelineF_Comp with initial value f2 "Value: 5" = false. Both start with false.
    • Updates: If we execute timelineIntF |> TL.define Now 1234567890. LHS: f1 1234567890 is “Value: 1234567890”. f2 "Value: 1234567890" is true (length 17 > 10). lhsTimelineF_Comp updates to true. RHS: timelineIntF |> TL.map f1 updates its intermediate Timeline<string> to “Value: 1234567890”. This change propagates, triggering the second TL.map f2, which updates rhsTimelineF_Comp to f2 "Value: 1234567890" = true. Both timelines exhibit identical value sequences.
  • Equivalence Explanation: The LHS applies the combined function f >> g within a single TL.map operation. The RHS pipes the timeline through TL.map f and then through TL.map g. The library’s internal dependency mechanism (DependencyCore) ensures that an update on the original timelineIntF correctly propagates through both stages in the RHS pipeline, yielding the same final value as the single combined step in the LHS. Therefore, the Composition Law holds (behaviorally).

Since both Functor laws hold behaviorally, Timeline is a valid Functor.


5.2 Verifying Monad Structure via Kleisli Composition

Section titled “5.2 Verifying Monad Structure via Kleisli Composition”

Now, we verify that Timeline forms a Monad. This relies on the TL.bind operation, TL.ID (often called return or pure), and Kleisli composition TL.(>>>).

The TL.bind Operation: Signature (from Timeline.fs, TL module): (* val bind<'a, 'b> : ('a -> Timeline<'b>) -> Timeline<'a> -> Timeline<'b> *) TL.bind handles functions that themselves return a Timeline: 'a -> Timeline<'b>. This enables chaining operations where the next step depends on the result of the previous, and that next step is also a Timeline.

Note on Monad Law Formulations: A type constructor T is a Monad if it’s a Functor and has ID : 'a -> T<'a> and bind : ('a -> T<'b>) -> T<'a> -> T<'b> (if bind is curried for pipelining, or T<'a> -> ('a -> T<'b>) -> T<'b>). The laws can be expressed using bind and ID.

An equivalent, and often more insightful, formulation defines a Monad through Kleisli composition. Kleisli arrows are functions of type 'a -> Timeline<'b>. A Monad structure exists if these arrows form a Monoid under a composition operation (here TL.(>>>)), with TL.ID as the Monoid’s identity. The laws are then the standard Monoid laws for TL.(>>>) and TL.ID. This perspective highlights the core algebraic structure.

Definitions for Timeline Monad (from Timeline.fs, TL module):

  • Kleisli Arrow (Monadic Function): A function with the signature 'a -> Timeline<'b>.
  • Identity Kleisli Arrow (TL.ID): (* val ID<'a> : 'a -> Timeline<'a> *) Takes a and wraps it in a new, static Timeline via Timeline a.
  • Kleisli Composition (TL.(>>>)): (* val inline (>>>) : ('a -> Timeline<'b>) -> ('b -> Timeline<'c>) -> ('a -> Timeline<'c>) *) Defined using TL.bind as: fun anArg -> (f anArg) |> TL.bind g. (Using anArg to avoid conflict with f and g function names in its own definition)

Setup for Monad Examples:

// Assume Timeline factory, Now, and TL module functions (TL.map, TL.bind, TL.ID, TL.define, TL.at, TL.(>>>)) are accessible
// --- Sample Kleisli Arrows (Monadic Functions) ---
// These functions (f, g, h) return a Timeline, distinguishing them from
// the simple functions used in the Functor examples (f1, f2).
// They are of the form 'a -> Timeline<'b>.
let f : int -> Timeline<string> = // Monadic Function f
fun i -> TL.ID (sprintf "f(%d)" i)
let g : string -> Timeline<float> = // Monadic Function g
fun s -> TL.ID (float s.Length)
let h : float -> Timeline<bool> = // Monadic Function h
fun fl -> TL.ID (fl > 4.0)
// Initial timeline for testing
let initialValueMonad : int = 10
let timelineMonadA : Timeline<int> = Timeline initialValueMonad

Verification of Kleisli Monoid Laws:

Law 1: Associativity Equation: (f >>> g) >>> h = f >>> (g >>> h)

  • Goal: Show timelineMonadA |> TL.bind ((f >>> g) >>> h) behaves identically to timelineMonadA |> TL.bind (f >>> (g >>> h)).

  • Illustrative Code:

    // --- LHS ---
    let fg_kleisli : int -> Timeline<float> = f >>> g
    let lhsFn_kleisli : int -> Timeline<bool> = fg_kleisli >>> h
    let lhsTimelineM : Timeline<bool> = timelineMonadA |> TL.bind lhsFn_kleisli
    // --- RHS ---
    let gh_kleisli : string -> Timeline<bool> = g >>> h
    let rhsFn_kleisli : int -> Timeline<bool> = f >>> gh_kleisli
    let rhsTimelineM : Timeline<bool> = timelineMonadA |> TL.bind rhsFn_kleisli
  • Verification (Conceptual):

    • Initial Values: Both lhsTimelineM and rhsTimelineM should initially yield true. (Path: 10 -> f -> Timeline "f(10)" -> g -> Timeline 5.0 -> h -> Timeline true).
    • Updates: If timelineMonadA |> TL.define Now 2. Both should subsequently yield false. (Path: 2 -> f -> Timeline "f(2)" -> g -> Timeline 4.0 -> h -> Timeline false).
  • Equivalence Explanation: The TL.bind implementation in Timeline.fs (which correctly handles scope and dependency switching) ensures that chaining binds, or equivalently composing Kleisli arrows with TL.(>>>), correctly propagates values according to the Monad laws. The associativity of TL.bind (and thus TL.(>>>)) is fundamental to this. Therefore, the Associativity Law holds (behaviorally).

Law 2: Left Identity Equation: TL.ID >>> f = f

  • Goal: Show timelineMonadA |> TL.bind (TL.ID >>> f) behaves identically to timelineMonadA |> TL.bind f.

  • Illustrative Code:

    // --- LHS ---
    let lhsFn_leftId : int -> Timeline<string> = TL.ID >>> f
    let lhsTimeline_leftId : Timeline<string> = timelineMonadA |> TL.bind lhsFn_leftId
    // --- RHS ---
    let rhsTimeline_leftId : Timeline<string> = timelineMonadA |> TL.bind f
  • Verification (Conceptual): Both initially yield Timeline "f(10)". Updates follow similarly.

  • Equivalence Explanation: The standard Monad law return a >>= f_kleisli is equivalent to f_kleisli a. TL.ID is return, TL.bind is analogous to >>=. The Kleisli composition TL.ID >>> f applied to an initial value a results in (TL.ID a) |> TL.bind f, which by Monad laws simplifies to f a. Therefore, the Left Identity Law holds (behaviorally).

Law 3: Right Identity Equation: f >>> TL.ID = f

  • Goal: Show timelineMonadA |> TL.bind (f >>> TL.ID) behaves identically to timelineMonadA |> TL.bind f.

  • Illustrative Code:

    // --- LHS ---
    let lhsFn_rightId : int -> Timeline<string> = f >>> TL.ID
    let lhsTimeline_rightId : Timeline<string> = timelineMonadA |> TL.bind lhsFn_rightId
    // --- RHS ---
    let rhsTimeline_rightId : Timeline<string> = timelineMonadA |> TL.bind f
  • Verification (Conceptual): Both initially yield Timeline "f(10)".

  • Equivalence Explanation: The standard Monad law m >>= return is equivalent to m. f >>> TL.ID applied to an initial value a yields (f a) |> TL.bind TL.ID. If f a results in a monadic value m (a Timeline in this case), then this is m |> TL.bind TL.ID, which by Monad laws simplifies to m. Therefore, the Right Identity Law holds (behaviorally).

Since Timeline supports an identity Kleisli arrow TL.ID and an associative Kleisli composition TL.(>>>) for which TL.ID is the left and right identity, Timeline satisfies the definition of a Monad based on the Kleisli Monoid formulation.


Having verified that Timeline is both a Functor and a Monad (specifically, that its Kleisli composition forms a Monoid), we can now discuss how these properties contribute to system robustness.

Functor Properties: The Functor laws guarantee that transforming Timeline values using TL.map is well-behaved:

  • Identity: t |> TL.map id is equivalent to t.
  • Composition: t |> TL.map (f1 >> f2) (using the simple functions f1, f2 from Functor setup) is equivalent to t |> TL.map f1 |> TL.map f2. This ensures predictable transformations that can be safely refactored.

Monad (Kleisli Monoid) Properties: The Monad structure, via Kleisli Monoid laws, guarantees reliable sequencing of Timeline-producing computations using TL.bind or TL.(>>>) (using the monadic functions f, g, h):

  • Identity (TL.ID): Neutral element for sequencing.
  • Associativity (TL.(>>>)): Crucially, (f >>> g) >>> h behaves identically to f >>> (g >>> h). Grouping doesn’t matter.

Concrete Benefits of Associativity:

  • Predictability: Result depends only on operations and sequence, not grouping.
  • Composability: Build complex pipelines reliably.
  • Refactoring Safety: Rearrange intermediate steps without altering overall logic. The Monad structure mathematically guarantees the elimination of errors related to the structure of sequential computations.

Conclusion (for Section 5.3): Adherence to Functor and Monad laws provides a strong foundation for robust, predictable, and composable systems with Timeline.


5.4 Obviousness from the Immutable Dependency Graph Model

Section titled “5.4 Obviousness from the Immutable Dependency Graph Model”

While the preceding sections provided arguments based on algebraic laws, the Functor and Monad properties of Timeline can also be seen as an obvious consequence of the immutable dependency graph model (introduced in Chapter 3).

Operations like TL.map and TL.bind build this graph:

  • t |> TL.map f_simple: Creates a new node directly dependent on t; its value is always f_simple applied to t’s current value.
  • t |> TL.bind f_monadic: Creates a new node whose value depends on the Timeline from f_monadic applied to t’s current value, establishing potentially dynamic dependencies.

The Functor and Monad laws essentially state that the final input-output relationship is independent of intermediate grouping or definition order of these graph-building operations.

  • Functor Composition: t -> intermediate -> result (via TL.map f1 |> TL.map f2) yields the same overall dependency as t -> result (via TL.map (f1 >> f2)).
  • Monad Associativity: Building A -> B -> C -> D by first defining A -> C (via f >>> g) and then C -> D (via >>> h) results in the same graph structure and information flow as defining B -> D first (via g >>> h) and then A -> B (via f >>> ...).

This inherent associativity of constructing dependency graphs is why, when TL.map and TL.bind consistently implement this model, Functor and Monad properties emerge naturally. The algebraic laws confirm the robustness inherent in declaratively defining relationships within this immutable graph framework.