Table of Contents

Core concepts

SodaFlow has a small vocabulary. Almost everything you build is a composition of four ideas, and the whole library follows once you have them.

Stream

A Stream<T> is a sequence of discrete events, each carrying a value of type T. It has no value between firings — "what is this stream's value right now?" is not a meaningful question. Button clicks, keystrokes, network responses, and timer expiries are streams.

Create one with Stream.Never<T>(), which fires nothing, or Stream.CreateSink<T>(), which you push into from imperative code with Send.

Cell

A Cell<T> holds a value that changes at discrete points in time. Unlike a stream it always has a current value, so Listen on a cell fires immediately with that value and then again on every change. Cells are what you bind UI to.

Create one with Cell.Constant(value), Cell.CreateSink(initial), or — most often — by calling Hold on a stream. Hold is the bridge from the discrete world to the stateful one:

Cell<string> lastClicked = clickedNames.Hold("none yet");

Accum is Hold with a fold in front of it, and it also gives back a cell:

Cell<int> count = clicks.Accum(0, static (_, n) => n + 1);

Behavior

A Behavior<T> is a value defined at every point in time, not just at discrete steps. Cells and behaviors are two views of the same underlying idea: Cell is the discrete, stepwise one and Behavior the continuous one.

The API surfaces this asymmetry deliberately. A cell has Updates, Values, and Listen; a behavior has none of them. You cannot ask a behavior when it changed, because a continuous value does not have a well-defined set of change moments — and being able to detect them would break the model's guarantees. c.AsBehavior() converts freely in the direction that discards information; going the other way requires the Operational primitives and their caveats.

If you are unsure which to reach for, use Cell. Reach for Behavior when the value genuinely is continuous — time being the canonical case.

Transaction

Everything happens inside a transaction, and this is the property that makes SodaFlow worth using. Within one transaction the entire dependency graph updates atomically: no listener observes a half-updated world, and a value derived from two sources that both changed simultaneously is computed once, from the new value of both.

That is the "no glitches" guarantee, and it is the thing an event bus cannot give you. Transactions covers what it buys you, when you need an explicit one, and what simultaneity means.

How they fit together

imperative code
      │  Send
      ▼
  StreamSink ──────────────┐
      │                    │
      │ Map, Filter,       │
      │ Merge, Snapshot    │
      ▼                    │
   Stream ── Hold ──────────► Cell ── AsBehavior ──► Behavior
      │                          │                        │
      │ Listen                   │ Listen, Lift, Calm     │ Sample, Lift
      ▼                          ▼                        ▼
  side effects               UI binding              continuous values

Streams carry events, Hold turns them into state, and Snapshot reads that state back when the next event arrives. Almost every SodaFlow program is that cycle, sometimes closed into a feedback loop.

The cycle, end to end

A shopping basket is small enough to read in one go and walks the whole cycle. Items arrive as events, the basket is state, and the checkout button reads that state back at the moment it is pressed.

StreamSink<Item> added = Stream.CreateSink<Item>();
StreamSink<Unit> checkedOut = Stream.CreateSink<Unit>();

// Stream to cell: fold the additions into the basket.
Cell<ImmutableList<Item>> basket = added.Accum(
    ImmutableList<Item>.Empty,
    static (item, items) => items.Add(item));

// Cell to cell: a derived value, recomputed for you.
Cell<decimal> total = basket.Map(static items => items.Sum(static i => i.Price));

// Cell back to stream: read the total at the instant the button is pressed.
Stream<decimal> orders = checkedOut.Snapshot(total, static (_, t) => t);

IListener l = orders.ListenStrong(t => Console.WriteLine($"charging {t:C}"));

Nothing here holds a total field or remembers to update one. total is defined as the sum of the basket, so it cannot disagree with it, and orders carries the total that was correct at the instant of the press rather than whatever a field happened to contain.

Where to go next

Topic Page
Every operation, C# and F# side by side Operation reference
Atomicity, simultaneity, Post Transactions
Values that depend on their own past Feedback loops
Graphs whose shape changes at runtime Switch
Clocks, alarms, deterministic tests Time and timers
Subscriptions and garbage collection Listener lifetimes
A large keyed collection behind a few bound rows Reactive collections
Maybe, Either, Unit SodaFlow.Functional
The formal meaning of every primitive Denotational semantics