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.
👆 Click above — FxDart 101 is a guided course with a live in-browser playground for every function; Daily Ledger is a full app built with fxdart; Dart vs FxDart puts native Dart and fxdart solutions side by side, with an honest verdict on each; RxDart vs FxDart runs the same 50-example format against RxDart — push streams vs pull pipelines, including the cases where RxDart is simply the right tool.
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 computes 3 results. - Concurrency you can dial —
concurrent(n)evaluates the upstream chainnitems 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 nativeIterables, so everything interops with ordinary Dart code. - One mental model for sync and async — the same operator names work on
Iterable(sync) andFxAsyncIterable(async), withStreambridges in both directions. - Typed errors — Kotlin Arrow 2.x's
Raise/Eitherapproach, ported: straight-lineeitherblocks instead offlatMappyramids, error accumulation withNonEmptyList, and validation fused directly into the concurrent pipelines above.
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:
skills/fxdart-pipelines/— collections, concurrent Futures, Streams, and complex flow logic.skills/fxdart-typed-errors/— the typed-error system:eitherblocks, error accumulation, and Either-aware pipeline validation.
Install it with the community skills CLI
(auto-detects your IDE/agent):
dart pub global activate skills
skills get fxdart
Or with 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: claude (.claude/skills/), codex / antigravity /
generic (.agents/skills/), devin (.devin/skills/), opencode
(.opencode/skills/), pi (.pi/skills/, global ~/.pi/agent/skills/).
--list shows install status; --remove uninstalls.
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) is the completion-order variant (faster first results, no
ordering guarantee). This back-channel protocol is why fxdart has its own
FxAsyncIterable instead of building on push-based Streams, which cannot
express it.
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
| Category | Functions |
|---|---|
| Generate | range, repeat, cycle, entries, keys, values |
| Transform (lazy) | map, mapEffect, flatMap, flat, scan, scan1, peek, pluck |
| Filter (lazy) | filter, reject, compact, uniq, uniqBy, difference(By), intersection(By), compress |
| Slice (lazy) | take, takeRight, takeWhile, takeUntilInclusive, drop, dropRight, dropWhile, dropUntil, slice, chunk, split |
| Combine (lazy) | append, prepend, concat, zip, zip3, zipWith, zipWithIndex, transpose, reverse, fork |
| Aggregate | reduce, fold, reduceLazy, toList, sum, sumBy, average, averageBy, min, minBy, max, maxBy, size, join, groupBy, indexBy, countBy, sort, sortBy, toSorted, partition, each, consume |
| Access | head, last, nth, find, findIndex, includes, isEmpty, every, some |
| Object (Map) | omit, pick, omitBy, pickBy, prop, props, evolve, fromEntries, compactObject, resolveProps, isMatch, matches |
| Function | pipe, pipe1, pipeLazy, identity, always, tap, apply, juxt, memoize, negate, not, when, unless, throwError, throwIf, cases, add, gt, gte, lt, lte, delay, sleep, unicodeToArray, .curried/.uncurried (extension getters, arity 2–5) |
| Predicates | isNull, isNotNull, isNil, isBoolean, isNumber, isString, isDate, isList, isMap |
| Async | every lazy/aggregate operator has an *Async twin (mapAsync, toListAsync, ...), plus toAsync, fromStream, concurrentAsync, concurrentPoolAsync, asyncEmpty |
| Typed errors | Either (Left/Right, fold, map, flatMap, recover, Either.catching), either/eitherAsync, nullable/nullableAsync, NonEmptyList/Nel, accumulate, zipOrAccumulate2..5, mapOrAccumulate, bindNel, toEitherNel, rights, lefts, separateEither, sequenceEither; chain terminals rights(), lefts(), separated(), sequence(), mapOrAccumulate() |
| Util | debounce, throttle, shuffle, createSeededRandom |
| Chains | fx() (sync, extends Iterable), fxAsync(), fxStream(); Fx<num>/FxAsync<num> gain sum/average/min/max |
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 |
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 keep @Deprecated stubs
(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: 850+ tests covering sync/async behavior, error propagation, laziness, 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.