fxdart 0.8.3
fxdart: ^0.8.3 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.