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