fxdart 0.8.0
fxdart: ^0.8.0 copied to clipboard
A functional programming library for Dart, ported from FxTS. Lazy evaluation, concurrent async iteration, and pipeline-style composition.
fxdart #
A functional programming library for Dart, ported from FxTS. Lazy evaluation, concurrent async iteration, and pipeline-style composition โ the FxTS programming model, rebuilt on Dart's type system.
// 6 requests of 1s complete in ~2s โ not ~6s.
await fx(userIds).toAsync().map(fetchUser).concurrent(3).toList();
๐ Try it in your browser #
๐ Click any badge above. Each one is a live, runnable site:
| Site | What it is | |
|---|---|---|
| ๐ | FxDart 101 | A guided course with an in-browser playground for every function |
| ๐ | Daily Ledger | A full app built with fxdart, running live |
| โ๏ธ | Dart vs FxDart | 50 problems solved both ways, with an honest verdict on each |
| โก | RxDart vs FxDart | The same 50-example format vs RxDart โ push streams vs pull pipelines, including the cases where RxDart is simply the right tool |
๐ Contents #
โจ Why fxdart? ยท ๐ฆ Install ยท ๐ค AI agent skills ยท ๐ ๏ธ Usage ยท ๐ API overview ยท ๐ Differences from FxTS ยท ๐งช Testing ยท ๐ Acknowledgments
โจ Why fxdart? #
๐ฆฅ Lazy evaluation #
Operators build a pipeline and do no work until a terminal operator runs โ so
fx(hugeList).map(f).filter(g).take(3) only ever computes 3 results.
๐ Concurrency you can dial #
concurrent(n) evaluates the upstream chain n items at a time while preserving
order โ turning six 1-second requests into a ~2-second batch with one method call.
๐ก๏ธ Type-safe pipelines #
The fx() chain keeps full static typing end to end. Sync operators are plain
functions over native Iterables, so everything interops with ordinary Dart code.
๐ง One mental model for sync and async #
The same operator names work on Iterable (sync) and FxAsyncIterable (async),
with Stream bridges in both directions.
๐ฏ Typed errors #
Kotlin Arrow 2.x's Raise/Either approach, ported: straight-line either blocks
instead of flatMap pyramids, error accumulation with NonEmptyList, and validation
fused directly into the concurrent pipelines above.
โก A push side too, when time matters #
Pull pipelines model data over demand; fxEvents() models events over time on
plain Dart Streams โ debounce, throttle, sample, switchMap, combineLatest
and friends โ then hands you back to the typed pull world with .pull().
๐ง Dart names work too #
Every FxTS name that Dart's collections already have a word for is also callable by
that word: where, expand, flattened, nonNulls, sorted, indexed,
firstWhereOrNull. No dialect to learn before you can read the code.
๐ฆ Install #
See the installation guide on pub.dev for the latest version.
๐ค AI agent skills #
fxdart ships two Agent Skills that teach AI coding
assistants โ Claude Code, Codex, Devin, Antigravity, OpenCode, pi, and
anything reading .agents/skills/ โ when and how to use fxdart:
| Skill | Covers |
|---|---|
๐ skills/fxdart-pipelines/ |
Collections, concurrent Futures, Streams, and complex flow logic |
๐ฏ skills/fxdart-typed-errors/ |
The typed-error system: either blocks, error accumulation, Either-aware pipeline validation |
Option A โ the community skills CLI (auto-detects your IDE/agent):
dart pub global activate skills
skills get fxdart
Option B โ fxdart's built-in zero-dependency installer:
# From a project that depends on fxdart:
dart run fxdart:install_skills # auto-detects agent dirs in the project
dart run fxdart:install_skills claude codex # or name agents explicitly
dart run fxdart:install_skills all --global # per-user dirs (~/.claude/skills, ~/.agents/skills, ...)
# Or standalone:
dart pub global activate fxdart
fxdart_skills --global claude
Supported agents:
| Agent | Install dir |
|---|---|
claude |
.claude/skills/ |
codex / antigravity / generic |
.agents/skills/ |
devin |
.devin/skills/ |
opencode |
.opencode/skills/ |
pi |
.pi/skills/ ยท global ~/.pi/agent/skills/ |
๐ก
--listshows install status ยท--removeuninstalls.
๐ ๏ธ Usage #
๐ Sync pipelines #
Sync operators are data-first functions over lazy Iterables; the fx() chain
composes them with full type inference:
import 'package:fxdart/fxdart.dart';
fx([1, 2, 3, 4, 5])
.map((a) => a + 10)
.filter((a) => a % 2 == 0)
.toList(); // [12, 14]
// Equivalent with top-level functions:
toList(filter((a) => a % 2 == 0, map((a) => a + 10, [1, 2, 3, 4, 5])));
// Laziness: only 3 squares are ever computed.
fx(range(1, 1000000)).map((a) => a * a).take(3).toList(); // [1, 4, 9]
โณ Async pipelines #
Async operators work on FxAsyncIterable<T> โ a pull-based protocol ported from
FxTS's AsyncIterable handling. Lift values in with toAsync / fromStream
(or .toAsync() on a chain), and out with .toList() / .toStream():
await fx([1, 2, 3, 4])
.toAsync()
.map((a) async => a + 10) // callbacks may be async
.filter((a) => a % 2 == 0)
.toList(); // [12, 14]
// Streams bridge both ways.
await fxStream(Stream.fromIterable([1, 2, 3])).map((a) => a * 2).toList();
โก Concurrency #
concurrent(n) is FxTS's signature feature, ported faithfully: a concurrency
marker travels backwards through the pipeline's iterator protocol, so the
upstream chain evaluates n items at once while results stay in order.
// 6 requests of 1s complete in ~2s instead of ~6s.
await fx([1, 2, 3, 4, 5, 6])
.toAsync()
.map((id) => fetchUser(id))
.concurrent(3)
.toList();
- ๐ฅ
concurrentPool(n)โ the completion-order variant: faster first results, no ordering guarantee.
โน๏ธ This back-channel protocol is why fxdart has its own
FxAsyncIterableinstead of building on push-basedStreams, which cannot express it.
๐ก Events (the push side) #
Some problems really are events over time, not data over demand. fxEvents()
wraps a plain Dart Stream in a chainable, Rx-flavoured API โ a thin wrapper, never
an extension, so it coexists with rxdart without member conflicts:
final results = await fxEvents(keystrokes)
.debounce(const Duration(milliseconds: 160))
.switchMap((q) => search(q).asStream()) // cancels the superseded search
.toList();
// Cross back into the typed pull world at any point:
await fxEvents(ticks).sampleOn(clock).pull().map(load).concurrent(4).toList();
LiveValue holds a current-value stream, and FxSubscriptions cancels a bag of
subscriptions together. See โก RxDart vs FxDart
for 50 worked examples โ including the cases where RxDart is the better fit.
๐ฏ Typed errors #
The either builder runs a block in a Raise<E> scope: each r.bind unwraps
a success or short-circuits the whole block with a typed failure โ the
Kotlin Arrow 2.x model, ported (no TaskEither/IO wrapper tower, no
Option; Dart's T? plus the nullable builder covers absence):
Either<String, int> parsePort(String raw) => either((r) {
final n = r.ensureNotNull(int.tryParse(raw), () => '"$raw" is not a number');
r.ensure(n > 0 && n < 65536, () => '$n is out of range');
return n;
});
// Validation accumulates EVERY failure into a NonEmptyList, not just the first:
final user = either<Nel<String>, User>((r) => r.zipOrAccumulate2(
(r) => validateName(r, input), (r) => validateAge(r, input), User.new));
// And it fuses with pipelines โ fail-slow, 8 records in flight, order kept:
final result = await fxStream(records)
.mapOrAccumulate<String, User>((r, rec) => parseUser(r, rec), concurrency: 8);
๐ Every subject has a detailed tutorial with an in-browser playground:
๐บ๏ธ overview ยท
โ๏ธ Either ยท
๐ฌ either & the Raise scope ยท
โ nullable ยท
๐ NonEmptyList ยท
โ accumulation ยท
๐ Either ร pipelines
๐ API overview #
๐ Differences from FxTS #
Dart has no function overloads, variadic generics, or conditional types, so some APIs deliberately deviate:
| FxTS | fxdart | |
|---|---|---|
| ๐ | curried data-last (map(f) inside pipe) |
fx() chain (typed) or dynamic pipe(value, [closures]) |
| ๐ | one map dispatching sync/async |
map (Iterable) / mapAsync (FxAsyncIterable); chains use plain names |
| ๐ | reduce(f, seed, iter) overload |
fold(seed, f, iter) (unseeded reduce(f, iter) unchanged) |
| ๐ฆ | tuples (zip, entries, partition) |
Dart records: (A, B) |
| ๐๏ธ | TS objects (omit, pick, evolve, โฆ) |
Map-based equivalents |
| โ | undefined |
null (head/find/nth return T?) |
| ๐ | toArray / toArrayAsync |
toList / toListAsync (Dart has no array type) |
| โณ | AsyncIterable / for await |
FxAsyncIterable + toStream() / fromStream() bridges |
| ๐๏ธ | variadic zip/juxt/cases |
fixed arities (zip/zip3) or list/record parameters |
| ๐ | curry(f) |
.curried / .uncurried extension getters โ see WHY_CURRIED.md |
๐ Why .curried instead of curry? #
FxTS's curry needs arity reflection and recursive conditional types, which
Dart lacks โ so fxdart curries through per-arity extensions instead, resolved
statically and fully typed:
int add(int a, int b) => a + b;
final addOne = add.curried(1); // int Function(int)
fx([1, 2, 3]).map(addOne).toList(); // [2, 3, 4]
๐ WHY_CURRIED.md tells the full design story: why the direct
port is impossible, how static extension resolution stands in for overloading,
why the getter is named curried, and how the same port-the-meaning
philosophy resolves the other unportable APIs.
โ ๏ธ Those APIs keep
@Deprecatedstubs (curry,isUndefined,isArray,isObject,takeUntil) so migrating code gets analyzer guidance instead of silent breakage.
๐งช Testing #
The FxTS spec suite has been ported alongside the library, and grown well past it: 1,800+ tests across 170 files, covering sync/async behavior, error propagation, laziness, typed errors, the events layer, and concurrency timing across every operator.
dart test
๐ Coverage is measured on every push and pull request and reported to Codecov. To reproduce locally:
dart run coverage:test_with_coverage # writes coverage/lcov.info
๐ Acknowledgments #
Great thanks to Indong Yoo, CTO of Marpple, the creator of FxTS (and FxJS before it), whose functional programming model โ lazy iteration with first-class, order-preserving concurrency โ this library ports to Dart. All core ideas, operator semantics, and the original test suite come from the marpple/FxTS repository.
๐ค Author #
Bansook Nam
- ๐ Website: https://github.com/bansooknam
- ๐ Github: @bansooknam
๐ค Contributing #
Contributions, issues and feature requests are welcome! Feel free to check the issues page.
๐ License #
Copyright ยฉ 2023 Bansook Nam.
This project is MIT licensed.
