Table of Contents

Feedback loops

Some values depend on their own past. A running total, a state machine, a position updated by velocity — each needs its previous value to compute its next one, which is a circular reference that ordinary construction cannot express: you would have to pass the value to itself before it exists.

Loops resolve this with a forward declaration. Declare a placeholder, build the graph that refers to it, then say what it was a reference to.

The functional form

This is the idiom to use. Stream.Loop<T>(), Cell.Loop<T>() and Behavior.Loop<T>() hand your block the placeholder — a LoopedStream<T>, LoopedCell<T> or LoopedBehavior<T> — take back the definition it returns, and close the loop for you, including the explicit transaction it needs.

StreamSink<int> s = Stream.CreateSink<int>();

// A running total: each firing adds to the total so far.
Stream<int> total = Stream.Loop<int>()
    .WithoutCaptures(l => s.Snapshot(l.Hold(0), (n, o) => n + o));

using (total.ListenStrong(Console.WriteLine))
{
    s.Send(1);   // 1
    s.Send(2);   // 3
    s.Send(3);   // 6
}

These samples use ListenStrong rather than Listen for one reason: using and use need IDisposable, and only IStrongListener implements it. Everywhere the handle is simply held in a variable, Listen is the one to reach for.

Read the C# version inside out: l is the placeholder for the very stream being defined — a LoopedStream<int>, which is a Stream<int> and usable as one anywhere — l.Hold(0) turns it into a cell of the running total starting at zero, and each firing of s snapshots that cell and adds to it. The result is l, which is what WithoutCaptures resolves.

Where a placeholder has to be passed somewhere that wants the plain type, AsStream, AsCell and AsBehavior say so without a cast.

Capturing extra values

Sometimes the loop body produces something else you want alongside the looped value. WithCaptures returns both:

StreamSink<int> s = Stream.CreateSink<int>();

(Stream<int> total, Stream<int> doubled) = Stream.Loop<int>()
    .WithCaptures(l => (
        Stream: s.Snapshot(l.Hold(0), (n, o) => n + o),
        Captures: s.Map(v => 2 * v)));

Stream is what the loop resolves to; Captures is anything else you want out. In F# the equivalent is loopS, which returns a struct tuple of the looped value and the captures, while loopWithNoCapturesS is the shorthand when there is nothing extra.

The explicit form

StreamLoop<T>, CellLoop<T> and BehaviorLoop<T> are the underlying mechanism, reached through Stream.CreateLoop<T>, Cell.CreateLoop<T> and Behavior.CreateLoop<T>. Each derives from the placeholder the block form hands out — StreamLoop<T> is a LoopedStream<T> — and adds the one member that closes it, Loop. You will meet them in older code and in the book's examples:

Transaction.RunVoid(() =>
{
    StreamLoop<int> l = Stream.CreateLoop<int>();
    Stream<int> total = s.Snapshot(l.Hold(0), (n, o) => n + o);
    l.Loop(total);
});

Four rules apply, and the library enforces all four with exceptions:

Mistake Message
Creating a loop outside a transaction Loop must be created within an explicit transaction.
Never calling Loop Loop was not looped.
Calling Loop twice Loop was looped more than once.
Closing it in a different transaction Loop must be looped in the same transaction that it was created in.

The functional form exists because it makes all four impossible, and it is the type system that makes them so rather than a convention. What the block is handed is a LoopedStream<T>, which has no Loop on it at all: there is nothing to call twice, nothing to leave uncalled, and nothing to call in the wrong transaction. The placeholder cannot outlive the block, and the definition is what that block returns rather than something a later statement has to remember to do.

So reach for Loop and write the explicit form only in the rare case where the loop cannot be scoped in a block at all — where the placeholder and its definition are separated by something a lambda cannot enclose. If what is being built is an object rather than a single value, that is not one of those cases; see below.

Sample inside a loop

Calling Sample on a looped cell while still constructing the loop asks for a value that does not exist yet. Use SampleLazy (sampleLazyC) instead — it defers the read until the loop has closed and there is something to read.

The same reasoning gives every operation that takes a starting value a deferred counterpart, for when that starting value is itself read out of the loop: Hold has HoldLazy, Accum has AccumLazy, Collect has CollectLazy, and Cell.Constant has Cell.ConstantLazy. Each takes a Lazy<T> where the eager one takes a value, and the F# aliases follow the same rule (holdLazyS, accumLazyS, collectLazyS).

The two go together more often than not, because a fold seeded from the collection it folds over needs both:

Cell<ImmutableHashSet<int>> drainable =
    accounts.ItemChangesStream.AccumLazy(
        // The keys already in the collection, read lazily because this is that collection.
        initialState: accounts.SnapshotCell.SampleLazy().Map(/* ... */),
        // The keys each change adds or removes.
        f: static (changes, keys) => /* ... */);

That is from the Accounts sample: the set it accumulates has to start from the accounts already there, which is a read of the very collection whose changes it is folding.

Forward references to a single value

A loop lets a cell be referred to before it exists. ForwardReference is the same idea with the cell taken back out — for when what has to refer to itself is one value, not a series of them:

Node node = ForwardReference<Node>.WithoutCaptures(
    reference => new Node(new Child(reference.AsCell())));

The block is handed a LoopedCell<Node>, and AsCell passes it on as the Cell<Node> the child wants — a cell that means nothing until the call returns and holds the finished node from then on. It unties the knot two objects make when each needs the other at construction, which otherwise forces one of them to be built half-formed and completed afterward — with a settable member that has no business being settable once the graph is up.

WithCaptures, and forwardReference in F#, return anything else worth keeping from the construction, the same way a loop does.

An object under construction

This is the form to use whenever a factory builds an object whose own graph feeds it — a view model whose buttons edit the collection its rows come from, say. Rather than declaring a loop per stream, build the whole object inside one ForwardReference<T>, keep each stream or cell that has to be looped in a private field of the object — or use the property it is already exposed through, where it has one — and read it back off the looped object with SwitchS or SwitchC:

public static IAccountsViewModel Create() =>
    Transaction.Run(static () =>
        ForwardReference<AccountsViewModel>.WithoutCaptures(static viewModelLoop =>
        {
            // The collection is fed from edits that do not exist until the rows do, and the rows
            // come from the collection. Edit is the collection's edit type, shortened here.
            Stream<Edit> depositsLoop = viewModelLoop.Map(static vm => vm.deposits).SwitchS();

            ReactiveCollection<int, AccountIdentity, AccountState> accounts =
                ReactiveCollection.Create(initialEntries: AccountSeed.Items, depositsLoop);

            // ... the rest of the graph, ending with the deposits the rows on the page send ...

            return new AccountsViewModel(rows: rows, deposits: deposits);
        }));

The cell the forward reference hands out holds the finished object, so SwitchS on a field of it is the same stream the object ends up with. One reference stands in for every looped value the object carries, which keeps the count of loops at one however many streams and cells feed back. In F# the same shape is forwardReferenceWithNoCaptures with switchS or switchC.

The C# value type sits on ForwardReference<T> rather than on the methods, which is what leaves the capture type free to be inferred — a lambda gives inference nothing to work from, and C# does not allow only some of a method's type arguments to be given. F# needs none of that: both types are inferred from the function, so neither is ever written.

Internally this is a cell loop closed with a constant cell, so the rule above applies unchanged: reading the reference during construction asks a question that has no answer yet, and throws.

Where this shows up

Accumulators are the common case, and Accum and Collect are loops with the plumbing already done:

Cell<int> total     = s.Accum(0, (v, acc) => v + acc);
Stream<string> outp = s.Collect(0, (v, st) => (ReturnValue: $"#{st}", State: st + 1));

Reach for those first. Write the loop by hand when the feedback path runs through logic that Accum and Collect cannot express — typically when it passes through other cells, or through a Switch.