Overview
Why a new collections library?
Dart's built-in List, Map, and Set are mutable by default. Any code that holds a
reference to a collection can silently change it, which forces defensive copying and makes
shared state hard to reason about.
Ribs replaces them with persistent, immutable-by-default equivalents providing these benefits:
- Immutability — every update operation returns a new collection; the original is never modified. Collections can be freely passed between functions and stored in state without defensive copies or surprise mutations.
- Structural sharing — because values are immutable, new collections can reuse internal nodes from the old one.
- Null-safe lookups —
get(key)returnsOption<V>instead of a nullableV?, making absent-key handling explicit and composable rather than a source ofNullPointerExceptions. - Richer API — operations like
updatedWith,groupMap,partitionMap,traverseEither, andsubsetsare built in, reducing the need for hand-written loops. - Integration with ribs — types such as
IO,State, andRillwork directly withIList,IMap, and friends; the whole library speaks the same collection language.
Tradeoffs: the API differs from Dart's standard collections — there is a small
learning curve. Code that passes collections to third-party libraries expecting a plain
List, Map, or Set needs a conversion step (toList(), toMap(), etc.).
The Collection Hierarchy
Every collection type in ribs — IList, IVector, IMap, ISet, and
others — is built on a three-level hierarchy of mixins and an abstract class.
Understanding this hierarchy explains where each method comes from and how
collections interoperate.
RIterableOnce<A> can be traversed once; the base of everything
├── RIterable<A> can be traversed multiple times; all concrete collections
│ ├── IList<A>
│ ├── IVector<A>
│ ├── ISet<A>
│ └── IMap<K, V> ...and all other concrete types
└── RIterator<A> a single-use cursor; produced by calling .iterator
RIterableOnce
RIterableOnce<A> is the foundation. Any type that mixes in RIterableOnce
can produce an RIterator<A> via its iterator getter, and that iterator can
be used to traverse the elements exactly once.
The mixin provides a large set of operations that work on any traversable
value — regardless of whether it is a full collection or a one-shot cursor.
All of these operations consume the RIterator at most once.
Properties
| Member | Description |
|---|---|
iterator | Returns an RIterator<A> for traversing the elements |
isEmpty | true if the collection contains no elements |
nonEmpty | true if the collection contains at least one element |
size | The number of elements; uses knownSize where available |
knownSize | The size if known without traversal, or -1 |
Transforming Elements
| Member | Description |
|---|---|
map(f) | Apply f to every element; return a new collection of results |
flatMap(f) | Apply f to every element, where f returns a collection; flatten results |
filter(p) | Keep only elements satisfying predicate p |
filterNot(p) | Keep only elements that do not satisfy predicate p |
collect(f) | Apply f to each element, keeping only Some results |
collectFirst(f) | Apply f to each element, returning the first Some |
tapEach(f) | Apply f for its side effect on each element; return the collection unchanged |
Folding and Reducing
| Member | Description |
|---|---|
foldLeft(z, op) | Accumulate a result left-to-right, starting with seed z |
foldRight(z, op) | Accumulate a result right-to-left, starting with seed z |
reduce(op) | Reduce to a single value with op; throws on empty collection |
reduceOption(op) | Like reduce, but returns None instead of throwing for empty |
reduceLeft(op) | Left-to-right reduce |
reduceLeftOption(op) | Left-to-right reduce returning None on empty |
reduceRight(op) | Right-to-left reduce |
reduceRightOption(op) | Right-to-left reduce returning None on empty |
Querying
| Member | Description |
|---|---|
find(p) | Return the first element satisfying p as Some, or None |
exists(p) | true if any element satisfies p |
forall(p) | true if all elements satisfy p |
count(p) | The number of elements satisfying p |
Slicing and Scanning
| Member | Description |
|---|---|
take(n) | The first n elements |
drop(n) | All elements after the first n |
slice(from, until) | Elements in the half-open range [from, until) |
takeWhile(p) | Elements from the start while p holds |
dropWhile(p) | Elements from the point where p first fails |
span(p) | A tuple of (takeWhile(p), dropWhile(p)) |
splitAt(n) | A tuple of (take(n), drop(n)) |
scan(z, op) | Running accumulation left-to-right, including the seed |
scanLeft(z, op) | Same as scan |
Converting
| Member | Description |
|---|---|
toList() | A Dart List<A> |
toIList() | An IList<A> |
toISet() | An ISet<A>, with duplicates removed |
toIVector() | An IVector<A> |
toIndexedSeq() | An IndexedSeq<A> |
mkString(...) | A String of elements joined by a separator, with optional start/end delimiters |
maxOption(order) | The largest element according to order, or None if empty |
minOption(order) | The smallest element according to order, or None if empty |
maxByOption(f, order) | The element with the largest projected value, or None if empty |
minByOption(f, order) | The element with the smallest projected value, or None if empty |
RIterable
RIterable<A> extends RIterableOnce<A> and guarantees that the collection
can be traversed any number of times. All concrete collection types in
ribs (IList, IVector, IMap, ISet, etc.) mix in RIterable. This
allows them to provide operations that require multiple passes or access to
structural positions such as the head, tail, or last element.
In addition to everything from RIterableOnce, RIterable provides:
Structural Access
| Member | Description |
|---|---|
head | The first element; throws if the collection is empty |
headOption | The first element as Some, or None if empty |
last | The last element; throws if the collection is empty |
lastOption | The last element as Some, or None if empty |
tail | All elements except the first |
init | All elements except the last |
tails | An iterator of progressively shorter suffixes, ending with empty |
inits | An iterator of progressively shorter prefixes, ending with empty |
Grouping and Partitioning
| Member | Description |
|---|---|
groupBy(f) | IMap keyed by f(element), values are sub-collections of matching elements |
groupMap(key, f) | Like groupBy, but also transforms each element with f |
groupMapReduce(key, f, reduce) | Like groupMap, but combines values sharing a key using reduce |
partition(p) | A tuple of two collections: elements satisfying p, and those that do not |
partitionMap(f) | Apply f returning Either; collect Left and Right results into separate collections |
grouped(n) | An iterator of non-overlapping chunks of size n |
sliding(size, step) | An iterator of overlapping windows of size size, advancing by step |
Combining
| Member | Description |
|---|---|
concat(suffix) | This collection followed by all elements of suffix |
zip(that) | Pair corresponding elements; length is the shorter of the two |
zipAll(that, thisElem, thatElem) | Pair corresponding elements; pad the shorter collection with fill elements |
zipWithIndex() | Pair each element with its zero-based index |
unzip() | (on RIterable<(A,B)>) Split a collection of pairs into two collections |
Additional Slicing
| Member | Description |
|---|---|
dropRight(n) | All elements except the last n |
takeRight(n) | The last n elements |
scanRight(z, op) | Running accumulation right-to-left |
RIterator
RIterator<A> is the concrete cursor type. It mixes in RIterableOnce<A>
and adds the classic hasNext/next() interface, inheriting all the
transformation and query operations from RIterableOnce on top of it.
An RIterator is single-use: once next() has been called to advance past
an element, that element is gone. The full collection API on RIterable is
built on top of creating fresh iterators from the underlying data structure.
Core Interface
| Member | Description |
|---|---|
hasNext | true if more elements remain |
next() | Returns the next element and advances; throws if empty |
toDart | Converts to a standard Dart Iterator<A> |
Static Constructors
RIterator provides factory methods for building iterators without a
backing collection:
| Method | Description |
|---|---|
RIterator.empty<A>() | An iterator with no elements |
RIterator.single(a) | An iterator over exactly one element |
RIterator.fill(n, elem) | An iterator that produces elem exactly n times |
RIterator.fromDart(it) | Wraps a standard Dart Iterator<A> |
RIterator.iterate(start, f) | Infinite iterator: start, f(start), f(f(start)), … |
RIterator.tabulate(n, f) | An iterator of n elements produced by f(0), f(1), …, f(n-1) |
RIterator.unfold(initial, f) | Stateful iterator: apply f to state, emit value and advance state, stop when f returns None |
RIterator.iterate and RIterator.unfold are especially useful for building
sequences without allocating a collection upfront. unfold is the most
general: it threads a piece of state through successive calls to f, emitting
one element per step, and stops as soon as f returns None.
Additional Operations on RIterator
Beyond what it inherits from RIterableOnce, RIterator provides:
| Member | Description |
|---|---|
concat(xs) | This iterator followed by the elements of xs |
distinct(f) | Deduplicate by key f, preserving first occurrence |
distinctBy(f) | Same as distinct |
grouped(n) | Consecutive non-overlapping chunks of size n |
sliding(size, step) | Overlapping windows |
padTo(len, elem) | Extend to at least len elements, padding with elem |
sameElements(that) | true if both iterators produce the same sequence of elements |
indexOf(elem) | Some(index) of the first occurrence of elem, or None |
indexWhere(p) | Some(index) of the first element satisfying p, or None |
zip(that) | Pair elements; stops at the shorter iterator |
zipAll(that, ...) | Pair elements; pads the shorter one |
zipWithIndex() | Pair each element with its index |