Cookbook
Recipes for things that come up constantly. Each one is short enough to read in full and adapt.
Ignore repeated values
Calm drops a firing whose value equals the one before it, so downstream work only happens on a
real change:
Stream<string> meaningful = keystrokes.Calm();
It compares with EqualityComparer<T>.Default; overloads take an IEqualityComparer<T> or a
plain Func<T, T, bool> when default equality is not what you want.
Calm is about values, not about timing — it is not a debounce. Typing ab, deleting the
b, and typing it again produces ab, a, ab, all of which get through. To rate-limit a
fast source, gate or schedule it with a timer instead.
Parse input, ignoring failures
Choose expresses "compute something that might not work, and only fire when it did" in one
step:
Stream<int> numbers = input.Choose(s => s.TryParseInt32());
TryParseInt32 is one of the parsing helpers on string, which answer with a
Maybe<T> instead of a bool and an out parameter. Written out by hand the selector would be
s => int.TryParse(s, out int n) ? Maybe.Some(n) : Maybe.None.
Map followed by FilterSome is the same thing spelled out, and is what to reach for when the
intermediate Stream<Maybe<T>> is wanted for its own sake:
Stream<int> numbers = input
.Map(s => s.TryParseInt32())
.FilterSome();
Enable a control conditionally
Gate drops firings while a Cell<bool> is false — no branching, no shape change:
Cell<bool> canSubmit = form.Map(f => f.IsValid);
Stream<Unit> submits = clicks.Gate(canSubmit);
Combine several values into one
Lift for a fixed set:
Cell<string> summary = firstName.Lift(lastName, (f, l) => $"{f} {l}");
Lift on a collection when the set is uniform:
Cell<IReadOnlyList<int>> allValues = cells.Lift();
Cell<int> total = allValues.Map(vs => vs.Sum());
Keep a running total
Accum is a loop with the plumbing already done. It folds a stream into a cell, so there is
no Hold to add afterwards:
Cell<int> total = amounts.Accum(0, static (v, acc) => v + acc);
When each firing needs to emit something different from the state it carries, Collect is
the general form:
// Emit a sequence number with each event.
Stream<string> numbered = events.Collect(
1,
(e, n) => (ReturnValue: $"{n}: {e}", State: n + 1));
Update two values atomically
Two sends are two transactions, and downstream sees two updates. Wrap them so it sees one:
Transaction.RunVoid(() =>
{
x.Send(newX);
y.Send(newY);
});
Anything lifted from both now fires exactly once, with both new values. See Transactions.
Take the first of several sources
Stream<Command> commands = new[] { fromKeyboard, fromMouse, fromNetwork }.OrElse();
OrElse is left-biased on simultaneity. If two can fire in the same transaction and you need
both values, use Merge with a combining function instead.
Fire once, then never again
Stream<Unit> firstLoad = dataArrived.Once();
For the imperative side of the same idea, ListenOnce unsubscribes itself, and
ListenOnceAsync gives you a Task<T> you can await. Hold the listener ListenOnce
returns until the firing arrives — it is weak, so nothing else keeps your handler alive — or
call ListenOnceStrong, which roots the stream until then. See
Listener lifetimes.
Time-stamp events
SecondsTimerSystem timers = new SecondsTimerSystem(ex => Log.Error(ex));
Stream<double> clickTimes = clicks.Snapshot(timers.Time, (_, t) => t);
See Time and timers for alarms and deterministic testing.
Search-as-you-type
The canonical async case: each keystroke cancels the in-flight request.
StreamSink<SearchResult> results = Stream.CreateSink<SearchResult>();
StreamSink<Exception> errors = Stream.CreateSink<Exception>();
AsyncMapStatus<string> status = queries.Calm().MapAsync(
results: results,
errors: errors,
operation: async (q, factory, token) => factory.FromValue(await SearchAsync(q, token)),
strategy: AsyncConcurrencyStrategy.SwitchLatest());
Cell<bool> spinner = status.IsRunning;
Calm first so identical queries do not re-fire; SwitchLatest so only the newest request
survives. See Asynchronous work.
Keep a large list on screen
A cell holding a list rebuilds the whole list on every edit, which is right for a search result —
one answer that changes as a whole — and wrong for a hundred thousand accounts behind a page of
six. A ReactiveCollection is the other shape: one stage per question, each keeping itself
current rather than being rebuilt.
ReactiveCollection<int, AccountIdentity, AccountState> accounts =
ReactiveCollection.Create(initialEntries: seed, deposits);
// One stage per question. Each keeps itself current as edits arrive.
ReactiveCollection<int, AccountIdentity, AccountState> page = accounts
.Filter(static (_, state) => !state.IsFrozen)
.SortBy(static (identity, _) => identity.Number)
.Slice(offsetCell: offset, limitCell: Cell.Constant(PageSize));
// One row object per key on the page, each bound to its own item.
MappedItems<AccountRowViewModel> rows = page.Map(
project: key => new AccountRowViewModel(
number: bindableFactory.CreateOneWay(
page.IdentityCell(key).Map(static identity => identity.Match(i => i.Number, () => string.Empty))),
balance: bindableFactory.CreateOneWay(
page.StateCell(key).Map(static state => state.Match(s => s.Balance, () => 0L)))),
onEvicted: static row => row.Dispose());
Bind the view to rows.Items. The important part is that each row's bindings are built from
IdentityCell and StateCell inside the projection, so every row follows its own item. That
is what makes an edit reach the row it is about rather than the list: a deposit moves one
balance while the other rows on the page, and the list itself, stay put.
Both cells answer with a Maybe<T>, which is why each projection has a Match in it. A row can
outlive the item it is bound to — the key leaves the view, the cell stops having a value, and an
add under the same key later gives it one again — so the empty case is a state the row has to
render rather than an error.
Two things decide whether this pays. An item is two halves — an identity that cannot change and a
state that can — so a sort over the identity can never be disturbed by an edit to the state,
which is why the example sorts on identity.Number. And Map ends the chain rather than
continuing it: what comes back is objects, which have no identity or state left for a further
Filter or SortBy to work on.
Each criteria is a cell where it needs to move — Filter takes a criteria cell, SortBy an
order cell, Slice an offset cell — so turning the page or clicking a header re-files the stage
already there instead of building a second chain to choose between.
See Reactive collections for what each stage costs, and the Accounts sample for this chain with a UI on it.
Longer worked examples
The book (Blackheath & Jones)
is the long-form treatment of this model, and is worth reading both for the Sodium basics
SodaFlow inherits and for Functional Reactive Programming generally. Its worked examples live in
the upstream Sodium repository under
book/, most of them in C# and F# as
well as Java. They are written against Sodium's API, so the namespaces and package names differ
from SodaFlow's, but the model and the operation names carry over directly — they remain the
best available long-form examples, and they compile.
| Example | Source | Demonstrates |
|---|---|---|
| Petrol pump | book/petrol-pump |
The flagship example: a complete state machine driving real UI. |
| Fridgets | book/fridgets |
Composable widgets built entirely from streams and cells. |
| Patterns | book/patterns |
Small, focused examples of recurring FRP patterns. |
| Operational | book/operational |
Correct use of the Operational primitives. |
| Continuous time | book/continuous-time |
Where Behavior earns its place over Cell. |
| Battle | book/battle |
A larger simulation. |
| Real world | book/real-world |
Integrating FRP with I/O and existing imperative code. |