Operation reference
The core FRP surface — streams, cells, behaviors, transactions — in both languages, in one
place. C# spells these as extension methods and static factories; F# offers qualified module
functions (Stream.map) and [<AutoOpen>] suffixed aliases (mapS). The alias column below
lists the latter, since that is what you get from open SodaFlow alone.
The add-on packages carry surfaces of their own, and each has its own page rather than a row
here: reactive collections, asynchronous work, data
binding, and the Maybe, Either and Unit helpers that come
with SodaFlow.Functional. Between them and this page, every public operation is described
somewhere.
For full signatures and parameter contracts, follow through to the generated API reference.
Creating
| C# | F# alias | Result |
|---|---|---|
Stream.Never<T>() |
neverS () |
A stream that never fires. |
Stream.CreateSink<T>() |
sinkS () |
A stream you can Send into. |
Stream.CreateSink<T>(coalesce) |
sinkWithCoalesceS f |
As above; coalesce combines multiple sends in one transaction. |
Cell.Constant(v) |
constantC v |
A cell that never changes. |
Cell.ConstantLazy(lazy) |
constantLazyC v |
As above, computed on demand. |
Cell.CreateSink(initial) |
sinkC initial |
A cell you can Send into. |
Cell.CreateStreamSink<T>() |
sinkCS () |
A stream sink whose firings are treated as cell updates. |
cs.AsStreamSink() |
— | The same object seen as the StreamSink<T> it already is, where one is wanted. |
Behavior.Constant(v) |
constantB v |
A behavior that never changes. |
Behavior.CreateSink(initial) |
sinkB initial |
A behavior you can Send into. |
Sinks are the boundary between imperative code and the FRP graph. Send into them from event handlers, timers, and network callbacks; everything downstream stays pure.
Stream operations
| C# | F# alias | Meaning |
|---|---|---|
s.Map(f) |
mapS f s |
Transform each value. |
s.MapTo(v) |
mapToS v s |
Replace each value with a constant. |
s.Filter(pred) |
filterS pred s |
Drop firings failing the predicate. |
s.FilterSome() |
filterSomeS s |
Unwrap Maybe<T> / option, dropping empties. |
s.Choose(f) |
chooseS f s |
Map and filter in one step: fire only the values f produced. |
s.Merge(s2, f) |
mergeS f (s, s2) |
Combine two streams; f resolves simultaneous firings. |
s.OrElse(s2) |
orElseS (s, s2) |
Combine two streams; on simultaneity the left wins. |
s.Merge(s2) |
mergeOptions2S (s, s2) |
Combine streams of different types into a tuple of Maybe<T> / option, one element per stream, set for each stream that fired. Up to eight streams. |
streams.Merge(f) |
mergeAllS f streams |
Merge a collection of streams. |
streams.OrElse() |
orElseAllS streams |
Left-biased merge of a collection. |
s.Hold(initial) |
holdS initial s |
Turn into a cell remembering the latest value. |
s.HoldLazy(lazy) |
holdLazyS v s |
As above with a lazy initial value. |
s.Snapshot(c) |
snapshotAndTakeC c s |
On each firing, take the cell's value, discarding the stream's. |
s.Snapshot(c, f) |
snapshotC c f s |
On each firing, combine the stream value with the cell's. |
s.Snapshot(c1, c2, f) |
snapshot2C c1 c2 f s |
Same, over more cells (up to 8). |
s.SnapshotLatest(c) |
snapshotLatestAndTakeC c s |
Like Snapshot(c), but sees the cell's value after this transaction. |
s.SnapshotLatest(c, f) |
snapshotLatestC c f s |
Like Snapshot(c, f), but sees the cell's value after this transaction. |
s.SnapshotLatest(c1, c2, f) |
snapshotLatest2C c1 c2 f s |
Same, over more cells (up to 8). |
s.Gate(c) |
gateC c s |
Drop firings while a Cell<bool> is false. |
s.Calm() |
calmS s |
Suppress firings equal to the previous one. |
s.Calm(comparer) |
calmWithEqualityComparerS cmp s |
As above with an explicit comparer. |
s.Accum(initial, f) |
accumS initial f s |
Fold into a cell holding the accumulated state. |
s.AccumLazy(initial, f) |
accumLazyS initial f s |
As above with a Lazy<T> initial state, for a fold whose starting value comes from the graph it is part of. |
s.Collect(initial, f) |
collectS initial f s |
Mealy machine: emit an output and a new state. |
s.CollectLazy(initial, f) |
collectLazyS initial f s |
As above with a Lazy<T> initial state. |
s.Once() |
onceS s |
Only the next firing, then never again. |
s.Listen(handler) |
listenS handler s |
Subscribe. Returns IWeakListener; does not keep the graph alive. |
s.ListenStrong(handler) |
listenStrongS handler s |
Subscribe and keep the graph alive. Returns IStrongListener. |
s.ListenOnce(handler) |
listenOnceS handler s |
Subscribe, then unsubscribe after one firing. Returns IWeakListener; does not keep the graph alive. |
s.ListenOnceStrong(handler) |
listenOnceStrongS handler s |
As above, keeping the graph alive until that firing. Returns IStrongListener. |
s.ListenOnceAsync() |
listenOnceAsyncS s |
The next firing as a Task<T>. |
The Snapshot family is the workhorse. Gate is Snapshot plus Filter; MapTo is Map
with a constant. Reach for the specific one — it reads better and does less work.
Snapshot sees the value a cell had before the current transaction, which is what lets a loop
read its own last state. When a change of one cell should read another cell after the same
change, use SnapshotLatest: c1.Values().SnapshotLatest(c2, f) fires only when c1 changes,
and gives c2's new value if the same transaction also updated c2. It cannot close a loop. It
takes up to eight cells; beyond that, give it a cell from Lift, whose value inside a transaction
is already the new one.
Cell operations
| C# | F# alias | Meaning |
|---|---|---|
c.Sample() |
sampleC c |
Read the current value now. An imperative escape hatch. |
c.SampleLazy() |
sampleLazyC c |
Read on demand, for use inside loops. |
c.Map(f) |
mapC f c |
Transform the value. |
c.Lift(c2, f) |
lift2C f (c, c2) |
Combine two cells. Overloads up to 8 cells in both languages. |
cells.Lift() |
liftAllC id cells |
Combine a collection into Cell<IReadOnlyList<T>>. F#'s liftAllC always takes a combining function; C# also has cells.Lift(f). |
c.Apply(cf) |
applyC cf c |
Apply a function held in a cell. The primitive Lift is built from. |
c.Calm() |
calmC c |
Suppress updates equal to the previous value. |
c.Updates() |
updatesC c |
The stream of changes. Does not fire on subscribe. |
c.Values() |
valuesC c |
Changes, plus one firing in the transaction it was obtained in. |
c.Listen(handler) |
listenC handler c |
Subscribe. Fires immediately with the current value. Does not keep the graph alive. |
c.ListenStrong(handler) |
listenStrongC handler c |
As above, and keeps the graph alive. |
c.AsBehavior() |
asBehaviorC c |
View this cell as a behavior. |
cc.SwitchC() |
switchC cc |
Flatten Cell<Cell<T>>. See Switch. |
cs.SwitchS() |
switchS cs |
Flatten Cell<Stream<T>>. |
cb.SwitchB() |
switchB cb |
Flatten Cell<Behavior<T>>. |
Updates versus Values
These differ in exactly one way, and it catches people out.
Updates gives you changes only. Values gives you changes plus an initial firing — but
only if you obtain and use it inside the same explicit transaction. Outside a transaction that
initial firing has nowhere to happen, and you silently get Updates behavior instead.
// Correct: the initial firing has a transaction to happen in.
IListener l = Transaction.Run(() => c.Values().Listen(Console.WriteLine));
If all you want is "current value, then changes", Listen on the cell already does exactly
that and needs none of the ceremony.
Behavior operations
Behavior<T> deliberately carries a much smaller surface than Cell<T>:
| C# | F# alias | Meaning |
|---|---|---|
b.Sample() |
sampleB b |
Read the current value. |
b.Map(f) |
mapB f b |
Transform the value. |
b.Lift(b2, f) |
lift2B f (b, b2) |
Combine behaviors. |
b.Apply(bf) |
applyB bf b |
Apply a function held in a behavior. |
bb.SwitchB() |
switchBB bb |
Flatten Behavior<Behavior<T>>. |
Note what is missing: a behavior has no Listen, no Updates, and no Values. That is
not an oversight. A behavior is defined at every point in time, so "the moments at which it
changes" is not a question the model is willing to answer — answering it would let you detect
steps that are meant to be invisible.
When you genuinely need those steps, Operational.Updates(b) and Operational.Value(b) will
hand them over, with the caveats below.
Going the other way, c.AsBehavior() converts a cell to a behavior freely, because that
direction discards information rather than inventing it.
Transactions
| C# | F# alias | Meaning |
|---|---|---|
Transaction.Run(f) |
runT f |
Run f in one transaction, returning its result. |
Transaction.RunVoid(a) |
runT |
Run an action in one transaction. |
Transaction.IsActive() |
isActiveT () |
Whether a transaction is currently running. |
Transaction.Post(a) |
postT a |
Run after the current transaction closes. |
Transaction.OnStart(a) |
onStartT a |
Run whenever any transaction starts. |
See Transactions for what this buys you and when you need to reach for it.
Operational primitives
| C# | Meaning |
|---|---|
Operational.Updates(b) |
The stream of steps of a behavior. |
Operational.Value(b) |
Fires once on listening with the current value, then on each step. |
Operational.Defer(s) |
Push firings into a subsequent transaction. |
Operational.Split(s) |
Turn a stream of collections into a stream firing once per element, each in its own transaction. |
The source is blunt about the first two: they are "not part of the main SodaFlow API" and they break the non-detectability of behavior steps. The stated rule is that you may use them only inside functions that do not let the caller detect those steps. In practice — fine in library internals, a smell in application code.
Defer and Split are different in kind. They concern transaction scheduling rather than
observability, and Split in particular is the normal, unremarkable way to fan a collection
out into individual firings.