fluent-iterable
The fluent iterator operations std forgot — joins, stable grouping, key
dedup, short-circuiting predicates — as one zero-dependency extension trait
over [std::iter::Iterator].
std iterators are excellent at map, filter and fold. The moment you
need to join two collections on a key, group preserving first-seen order,
deduplicate by projection, or ask "are there more than 20 rows?" without
draining a cursor, you are writing HashMap loops again. This crate ships
exactly that residue — the same operation names the
@codibre/fluent-iterable
ecosystem made familiar in JavaScript — on the one mechanism Rust actually
has: an extension trait with a blanket impl over every Iterator.
- No wrapper type. No
Fluent::new(iter), no intermediate object model. Import one trait and chain onto iterators you already have. - Zero dependencies. Everything is built from
stdyou already compile. - Honest laziness. Adapters stream and stop early; operations whose
first result is not final until the whole source is read
(
group_by_stable,combine_join, the sort family) materialize once and hand back a solidVec/HashMap— same work, same memory, plus re-iterability and everyVecmethod for free. - Honest typing.
avg()returnsOption<f64>, neverNaN.
use FluentIter;
let orders = ;
let customers = ;
// Inner join — streams the left side, indexes the right one exactly once.
let joined: = orders
.iter
.combine
.collect;
assert_eq!;
// Stable grouping: buckets in first-seen order, never alphabetical.
let by_customer = orders.iter.group_by_stable;
assert_eq!;
// Fold and project in one pass.
let report = orders
.iter
.map
.reduce_and_map;
assert_eq!;
The trait
One import — use fluent_iterable::FluentIter; — adds everything below to
every Iterator.
Joins & grouping
| Method | What it gives you | Execution |
|---|---|---|
combine(other, key_left, key_right) |
inner-join rows, streaming the left side | lazy adapter |
combine_join(key) |
merge a stream of row-sets into per-key groups | resolves once |
group_by_stable(key) |
buckets keyed by key, keys ordered by first appearance |
resolves once |
to_map_by(key, value) |
index into a HashMap (last write wins) |
resolves once |
to_map_chain(key1, key2) |
two-level HashMap<HashMap<K2, Vec<T>>> index |
resolves once |
to_map_chain_reduce(key1, key2, init, reduce) |
two-level index with fold-in aggregation | resolves once |
Distinctness & membership
| Method | What it gives you | Execution |
|---|---|---|
distinct() |
first occurrence of each item | lazy, buffers only seen keys |
distinct_by(key) |
first occurrence per projected key | lazy, buffers only seen keys |
is_distinct() / is_distinct_by(key) |
"any duplicates?" | short-circuits on the first repetition |
contains(item) |
membership check | short-circuits |
Fallbacks, repetition & cardinality guards
| Method | What it gives you | Execution |
|---|---|---|
when_empty(fallback) |
the source, unless it yields nothing | lazy |
repeat(times) |
the sequence, times times (streams pass 1, replays a cache after) |
lazy |
has_exactly(n) / has_less_than(n) / has_less_or_exactly(n) / has_more_than(n) / has_more_or_exactly(n) |
cardinality answers without draining | stop at n + 1 items |
Sorting, strings & numbers
| Method | What it gives you | Execution |
|---|---|---|
sorted() / sort_with(cmp) |
stable sort by Ord / custom comparator |
resolves once |
sort_by(key) / sort_by_desc(key) |
stable sort by projected key | resolves once |
join(sep) / join_by(value, sep) |
concatenate Displayables / projected values |
immediate |
avg() / avg_by(value) |
mean as Option<f64> — None on empty |
immediate |
Recipes
Pagination probe without draining the cursor — has_more_than stops at
page_size + 1:
use FluentIter;
let page = 20;
assert!;
assert!;
Deduplicate lazily — distinct_by holds one key set, never the items,
and take still short-circuits:
use FluentIter;
let events = ;
let first_per_kind: = events
.into_iter
.distinct_by
.take
.collect;
assert_eq!;
Streaming equi-join — the right side is indexed on the first pull; the
left side flows, so take(1) reads one left row:
use FluentIter;
let users = ;
let orders = ;
let pairs: = orders
.iter
.combine
.take
.collect;
assert_eq!;
Two-level index with aggregation — per customer, per month, total spend:
use FluentIter;
let sales = ;
let totals = sales
.iter
.to_map_chain_reduce;
assert_eq!;
Empty-source fallback and bounded replay:
use FluentIter;
let nothing: = Vecnew;
let values: = nothing.into_iter.when_empty.collect;
assert_eq!;
let replayed: = .into_iter.repeat.collect;
assert_eq!;
Report legibly:
use FluentIter;
let scores = ;
assert_eq!;
assert_eq!; // no NaN, ever
let label = scores.iter.join_by;
assert_eq!;
Design choices that differ from the JS original
avg()returnsOption<f64>— empty input isNone, notNaN.- Sorting is stable and typed —
Ord/comparators, never JS's string coercion; equal items keep input order. group_by_stableorders keys by first appearance — the behavior everyone expects from.group()and nobody gets fromHashMap.- Join keys are explicit closures — no implicit identity-key magic.
- Nested indices go two levels deep (
to_map_chain*) with concrete types; deeper nesting is a fold of two-level layers. - First-wins
distinct_by— a chooser/max-occurrences flavor can land later without breaking names. - Nothing async — stable Rust has no
AsyncIteratortrait; when the language lands generic async iteration, the vocabulary ports straight. Emitter/lifecycle ops (emit/catch/finally) areResult,Dropand stream-crate territory in Rust, by design.
Coming from npm fluent-iterable?
std already owns most of the surface: .map, .filter, .flatMap,
.mapNotNull → filter_map, .first → next/find, .some/.every →
any/all, .take/.skip/.takeWhile/.skipWhile, .count,
.sum/.min/.max, .reduce → fold, .partition → slice::chunks,
.concat/.append → chain, .withIndex → enumerate, .toArray →
collect, .toSet → collect::<HashSet<_>>(), .top → max_by_key. The
methods in the tables above cover the rest, one-for-one under the same
names (.group → group_by_stable, .combineJoin → combine_join,
.toMapChainReduce → to_map_chain_reduce, .hasLessOrExactly →
has_less_or_exactly, .reduceAndMap → reduce_and_map with the initial
value first, Rust-style).
Install
Then:
use FluentIter;
Edition 2021, MSRV 1.70, MIT licensed, zero dependencies.
Development
The crate keeps one source file per operation family: build.rs splices the
src/methods/*.rs fragments into the trait rustc compiles — edit the
fragments, never the generated src/fluent_iter.rs. Inside the monorepo the
crate rides the shared pnpm/turbo/release-it pipeline; see
RELEASING.md in the repository for the full release and trusted-publishing
runbook.