fx
Wraps a sequence in a lazy, chainable pipeline — the typed heart of FxDart.
Lecture
Everything in this course builds toward one idea: a chain.
fx(iterable) wraps any Iterable<T> in an
Fx<T> — an object with FxTS-style methods like
.map(), .filter(), and .take()
hung off it. Every one of those calls returns a new
Fx wrapping a bit more lazy computation. None of it runs
yet. Fx only starts doing work when you call a
terminal operator — toList(),
each(), consume(), reduce(), and
friends — which pulls values through the whole chain, one at a time,
from the terminal all the way back to the source.
This laziness is why FxDart can safely chain over huge or infinite
sequences (range, cycle, repeat):
as long as something downstream — usually take(n) — decides
how many values to actually pull, the upstream steps only ever run that
many times.
fx is the sync half of the chain. Its async
counterparts are fxAsync, which wraps an
FxAsyncIterable (the thing you get from toAsync,
fromStream, or any *Async function), and
fxStream, a shortcut that wraps a Dart Stream
directly. Both return an FxAsync<T> chain whose
methods accept functions that may return a Future, and
whose terminal operators all return a Future you
await. Switch from sync to async mid-chain with
.toAsync().
Why does this exist at all, instead of just calling top-level functions
like map(f, iterable)? Because Dart cannot type a variadic
pipe the way FxTS's TypeScript can (see the next lesson) —
fx() chaining is how FxDart gets fully typed, autocompletable
pipelines instead.
Fx<T> is now an extension type that
erases to the wrapped Iterable<T> at runtime. All
documented APIs stay identical — chains work exactly as before. What breaks:
x is Fx<T> checks (the type doesn't exist at runtime),
and code that tried to extend or implement Fx directly (use the
top-level functions instead). If you're using fx() the normal
way, your code needs no changes.
Demo 1 · Nothing runs until the terminal op
Watch calls stay at 0 right after building the chain, then
jump once toList() actually pulls the 5 values:
Demo 2 · fxAsync and fxStream
fxAsync wraps an FxAsyncIterable (here, from
toAsync); fxStream wraps a Stream
directly. Both give you the same chain methods, async-flavored:
The getter spelling
Every entry point also exists as a getter: .fx on an
Iterable, an FxAsyncIterable or a
Stream, .fxAsync on an iterable of futures, and
.fxEvents on a Stream. They build exactly the
same chain — the difference is only which end of the expression you read
from:
// the function: you go back to the front to open the paren
fx(orders.where(isPaid)).groupBy((o) => o.customerId);
// the getter: left to right, the way .toList() reads
orders.where(isPaid).fx.groupBy((o) => o.customerId);
This is the Dart-idiomatic spelling, and it is free: Fx is an
extension type, so the wrapper erases to the iterable itself, and a getter
whose body is this is a static call the compiler deletes. Over
a million-element map + filter + sum,
the two forms measure 12.640 ms and 12.665 ms — the same number twice.
These pages use fx() throughout. It is the
name FxTS uses, and it is the one that takes an explicit type argument,
which a getter cannot do postfix — fx<num>(xs) works
where xs.fx<num> does not parse. Pick whichever reads
better in your own code; they compile to the same thing.
One asymmetry is worth knowing. Over an
Iterable<Future<T>>, .fx gives you an
Fx<Future<T>> — a chain over the futures rather
than their values, which compiles and quietly does the wrong thing. That is
what .fxAsync is for: it awaits them, so T is the
resolved type and concurrent(n) has something to work with.
await responses.fxAsync.map(parse).concurrent(4).toList();
A Stream carries both getters, because it is the one source
that belongs to both worlds. .fx gives the pull
chain, the same as fxStream; .fxEvents gives the
push chain, the same as fxEvents —
debouncing, throttling, switching. Cross from push back to pull with
.pull(). The two are compared side by side in
Stream bridges.
keystrokes.fxEvents
.debounce(const Duration(milliseconds: 160))
.switchMap((q) => search(q).asStream())
.pull()
.toList();
Every getter spelling
The convention is one rule: an entry point carries fx in its
name. It says which library you are stepping into, and it keeps the bare
name — toAsync, shuffle, debounce —
free for whatever else a project puts on that type.
| Receiver | Getter | Same as |
|---|---|---|
Iterable<T> | .fx | fx(xs) |
FxAsyncIterable<T> | .fx | fxAsync(it) |
Stream<T> | .fx | fxStream(s) |
Iterable<FutureOr<T>> | .fxAsync | toAsync(xs) |
Stream<T> | .fxEvents | fxEvents(s) |
Stream<T> | .fxLive | LiveValue.from(s) |
Stream<T> | .fxLiveSeeded | LiveValue.seededFrom(v, s) |
Iterable<T> | .fxShuffle | shuffle(xs) |
FxAsyncIterable<T> | .fxShuffle | shuffleAsync(it) |
void Function(T) | .fxDebounce | debounce(f, w) |
void Function(T) | .fxThrottle | throttle(f, w) |
The operators themselves are not on this list, and will not be. Fifteen of
them — map, where, take,
fold and friends — share a name with a member
Iterable already has, and an instance member always beats an
extension, so those calls could never reach fxdart. The chain is where the
operators live: xs.fx.map(f), not xs.map(f).
Try it yourself
Exercise: build a chain that keeps scores of 60 or above, doubles them as bonus points, and takes only the first 2 results.