Expand description
Crate documentation lives in README.md — one source of truth for crates.io, docs.rs and this page.
§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 fluent_iterable::FluentIter;
let orders = [(1, "keyboard"), (2, "mouse"), (1, "monitor")];
let customers = [(1, "Ada"), (2, "Grace")];
// Inner join — streams the left side, indexes the right one exactly once.
let joined: Vec<_> = orders
.iter()
.combine(customers.iter(), |order| order.0, |customer| customer.0)
.collect();
assert_eq!(joined.len(), 3);
// Stable grouping: buckets in first-seen order, never alphabetical.
let by_customer = orders.iter().group_by_stable(|order| order.0);
assert_eq!(by_customer[0].1, vec![&(1, "keyboard"), &(1, "monitor")]);
// Fold and project in one pass.
let report = orders
.iter()
.map(|(_, name)| name.len())
.reduce_and_map(0usize, |sum, len| sum + len, |sum| format!("{sum} chars ordered"));
assert_eq!(report, "20 chars ordered");§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 fluent_iterable::FluentIter;
let page = 20;
assert!((0..21).has_more_than(page));
assert!(!(0..20).has_more_than(page));Deduplicate lazily — distinct_by holds one key set, never the items,
and take still short-circuits:
use fluent_iterable::FluentIter;
let events = [("click", 1), ("scroll", 2), ("click", 3)];
let first_per_kind: Vec<(&str, i32)> = events
.into_iter()
.distinct_by(|(kind, _)| *kind)
.take(1)
.collect();
assert_eq!(first_per_kind, vec![("click", 1)]);Streaming equi-join — the right side is indexed on the first pull; the
left side flows, so take(1) reads one left row:
use fluent_iterable::FluentIter;
let users = [(1, "Ada"), (2, "Grace")];
let orders = [(1, "keyboard"), (1, "monitor"), (2, "mouse")];
let pairs: Vec<_> = orders
.iter()
.combine(&users, |o| o.0, |u| u.0)
.take(1)
.collect();
assert_eq!(pairs.len(), 1);Two-level index with aggregation — per customer, per month, total spend:
use fluent_iterable::FluentIter;
let sales = [("ada", 3, 40.0), ("ada", 3, 10.0), ("grace", 4, 25.0)];
let totals = sales
.iter()
.to_map_chain_reduce(|s| s.0, |s| s.1, 0.0, |sum, s| sum + s.2);
assert_eq!(totals["ada"][&3], 50.0);Empty-source fallback and bounded replay:
use fluent_iterable::FluentIter;
let nothing: Vec<i32> = Vec::new();
let values: Vec<i32> = nothing.into_iter().when_empty([1, 2, 3]).collect();
assert_eq!(values, vec![1, 2, 3]);
let replayed: Vec<i32> = [1, 2].into_iter().repeat(3).collect();
assert_eq!(replayed, vec![1, 2, 1, 2, 1, 2]);Report legibly:
use fluent_iterable::FluentIter;
let scores = [7.5, 8.0, 9.5];
assert_eq!(scores.into_iter().avg(), Some(8.333333333333334));
assert_eq!(std::iter::empty::<f64>().avg(), None); // no NaN, ever
let label = scores.iter().join_by(|v| format!("{v:.1}"), " · ");
assert_eq!(label, "7.5 · 8.0 · 9.5");§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
cargo add fluent-iterableThen:
use fluent_iterable::FluentIter;Edition 2021, MSRV 1.70, MIT licensed, zero dependencies.
§Development
cargo test # unit + integration tests, and every example above
bash scripts/coverage.sh # writes coverage/lcov.info (bootstraps cargo-llvm-cov)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.
Re-exports§
pub use ops::combining::Combine;pub use ops::distinct::Distinct;pub use ops::distinct::DistinctBy;pub use ops::fallback::Repeat;pub use ops::fallback::WhenEmpty;
Modules§
- ops
- Streaming adapter structs behind the lazy operations. Each adapter
keeps its
Iteratorimplementation right next to itself.
Traits§
- Fluent
Iter - The missing combinators of
std::iter::Iterator, in the [@codibre/fluent-iterable] vocabulary (see the crate docs for the full npm ⇄ Rust map).