Skip to main content

Crate bulks

Crate bulks 

Source
Expand description

Composable bulk-iteration.

This crate adds Bulks, which are similar to iterators, except they are stricter. They can only be wholly consumed, where every value is operated on in bulk. This, unlike with classic Iterators, makes them fully compatible with arrays!

§Example

use bulks::*;

let a = [1, 2, 3];

let b: [_; _] = a.bulk()
    .copied()
    .map(|x| (x - 1) as usize)
    .enumerate()
    .inspect(|(i, x)| assert_eq!(i, x))
    .collect();

assert_eq!(b, [(0, 0), (1, 1), (2, 2)]);

§Constraints

Bulks are subject to some extra constraints that don’t affect normal iterators. In order for a bulk to be evaluated as is, the whole bulk must be consumed. Alternatively, it can be converted into an iterator to evaluate each iteration seperately. While iterators can be mutably exhausted, bulks cannot, and are therefore guaranteed to be intact.

Their constrained nature means fewer operations are possible, but the guarantees it gives makes it possible to use them with arrays while still retaining the array’s length. Operations that preserves the length of the data like map, zip, enumerate, rev and inspect are allowed. By enabling the generic_const_exprs-feature, some other length-modifying operations are also allowed such as flat_map, flatten, intersperse, array_chunks and map_windows since these modify the bulk’s length in a predetermined way. Of course, wholly consuming operations like fold, try_fold, reduce, try_reduce, collect and try_collect are fully supported. There’s also collect_array and try_collect_array to avoid turbofish-syntax when doing collect or try_collect.

Any Bulk that was created from an array can be collected back into an array, given that the operations done on it makes the length predetermined at compile-time. Bulks can also be used with other structures, allowing generic implementations that work the same on arrays as with other iterables.

§Bulk

The trait Bulk is similar to Iterator, but lacks the next method. Instead, its function is based on the for_each and try_for_each methods.

use core::ops::Try;

trait Bulk: IntoIterator
{
    fn len(&self) -> usize;

    fn for_each<F>(self, f: F)
    where
        Self: Sized,
        F: FnMut(Self::Item);

    fn try_for_each<F, R>(self, f: F) -> R
    where
        Self: Sized,
        F: FnMut(Self::Item) -> R,
        R: Try<Output = ()>;
}

Bulk’s full definition includes a number of other methods as well, but they are default methods, built on top of for_each and try_for_each, and so you get them for free.

Bulks are also composable, and it’s common to chain them together to do more complex forms of processing. See the Adapters section below for more details.

§The three forms of bulk-iteration

There are three common methods which can create bulks from a collection:

These are the in-bulk counterparts of iter(), iter_mut() and into_iter(). The trait IntoBulk provides the method into_bulk.

bulk() is available for any T where &T implements IntoBulk, and bulk_mut() is available for any T where &mut T is IntoBulk. They are just shorthand for doing into_bulk on a reference.

IntoBulk is automatically implemented for all Bulks. Other types that can be converted into an ExactSizeIterator through IntoIterator also automatically implement IntoBulk, converting them to a bulks::iter::Bulk, however this implementation can be specialized. For example, arrays specialize this implementation, converting to bulks::array::IntoBulk instead. Specializing IntoBulk is useful for collections whose length must be retained at compile-time, like arrays.

§Implementing Bulk

Making your own bulk is a bit similar to making an Iterator, but a little bit less convenient.

Your bulk needs a corresponding iterator that it can be converted to, which must be an ExactSizeIterator.

use core::ops::Try;

use bulks::*;

/// An iterator which counts from one to `N`
struct CounterIter<const N: usize>
{
    count: usize,
}

// we want our count to start at one, so let's add a new() method to help.
// This isn't strictly necessary, but is convenient.
impl<const N: usize> CounterIter<N>
{
    pub fn new() -> Self
    {
        CounterIter { count: 0 }
    }
}

// Then, we implement `Iterator` for our `CounterIter`:
impl<const N: usize> Iterator for CounterIter<N>
{
    // We will be counting with usize
    type Item = usize;

    // next() is the only required method
    fn next(&mut self) -> Option<Self::Item>
    {
        // Increment our count. This is why we started at zero.
        self.count += 1;

        // Check to see if we've finished counting or not.
        if self.count <= N
        {
            Some(self.count)
        }
        else
        {
            None
        }
    }

    // Since we're implementing `ExactSizeIterator`, it's a good idea to override `size_hint`.
    fn size_hint(&self) -> (usize, Option<usize>)
    {
        let len = self.len();
        (len, Some(len))
    }
}

// We also need our `CounterIter` to be an `ExactSizeIterator`.
impl<const N: usize> ExactSizeIterator for CounterIter<N>
{
    fn len(&self) -> usize
    {
        N.saturating_sub(self.count)
    }
}

// Now that we have an iterator we can start defining our bulk-iterator.

/// A bulk which counts from one to five
struct Counter<const N: usize>;

// Then, we implement `IntoIterator` for our `Counter`:
impl<const N: usize> IntoIterator for Counter<N>
{
    // We will be counting with usize
    type Item = usize;
    // This is iterator needs to be equivalent to our bulk.
    type IntoIter = CounterIter<N>;

    fn into_iter(self) -> Self::IntoIter
    {
        CounterIter::new()
    }
}

// Then, we implement `Bulk` for our `Counter`:
impl<const N: usize> Bulk for Counter<N>
{
    type MinLength = [(); N];
    type MaxLength = [(); N];

    fn len(&self) -> usize
    {
        N
    }

    fn for_each<F>(self, mut f: F)
    where
        Self: Sized,
        F: FnMut(Self::Item)
    {
        for i in self
        {
            f(i)
        }
    }

    fn try_for_each<F, R>(self, mut f: F) -> R
    where
        Self: Sized,
        F: FnMut(Self::Item) -> R,
        R: Try<Output = ()>
    {
        for i in self
        {
            f(i)?
        }
        R::from_output(())
    }
}

// And now we can use it!
let counter = Counter::<5>;
let result: [_; _] = counter.collect();

assert_eq!(result, [1, 2, 3, 4, 5]);

§Adapters

Just like with iterators there are adapters for bulks. These are functions which take a Bulk and return another Bulk.

Common bulk adapters include map, take, and rev. For more, see their documentation.

§Laziness

Bulks (and bulk adapters), just like iterators, are lazy. This means that just creating a bulk doesn’t do a whole lot. Nothing really happens until you consume it. This is sometimes a source of confusion when creating a bulk solely for its side effects. For example, the map method calls a closure on each element it iterates over:

use bulks::*;

let a = [1, 2, 3, 4, 5];
a.bulk().map(|x| println!("{x}"));

This will not print any values, as we only created a bulk, rather than using it. The compiler will warn us about this kind of behavior:

warning: unused result that must be used: bulks are lazy and
do nothing unless consumed

The idiomatic way to write a map for its side effects is to use a for loop or call the for_each method:

use bulks::*;

let a = [1, 2, 3, 4, 5];

a.bulk().for_each(|x| println!("{x}"));
// or
for x in &a
{
    println!("{x}");
}

Another common way to evaluate a bulk is to use the collect method to produce a new collection.

use bulks::*;

let a = [1, 2, 3, 4, 5];

let b: [_; _] = a.into_bulk().collect();

assert_eq!(a, b);

Modules§

array
iter
option
range
slice

Structs§

ArrayChunks
A bulk over N elements of the bulk at a time.
Chain
A bulk that links two bulks together, in a chain.
Cloned
A bulk that clones the elements of an underlying bulk.
Contained
Copied
A bulk that copies the elements of an underlying bulk.
Empty
A bulk that yields nothing.
Enumerate
A bulk that yields the element’s index and the element.
EnumerateFrom
A bulk that yields the element’s index counting from a given initial index and the element.
FlatMap
A bulk that maps each element to an iterator, and yields the elements of the produced bulks.
Flatten
A bulk that flattens one level of nesting in a of things that can be turned into bulks.
Inspect
A bulk that calls a function with a reference to each element before yielding it.
Intersperse
A bulk adapter that places a separator between all elements.
IntersperseWith
A bulk adapter that places a separator between all elements.
Map
A bulk that maps the values of bulk with f.
MapWindows
A bulk over the mapped windows of another bulk.
Mutate
A bulk that calls a function with a mutable reference to each element before yielding it.
Once
A bulk that yields an element exactly once.
OnceWith
A bulk that yields a single element of type A by applying the provided closure F: FnOnce() -> A.
OutOfRange
RepeatN
A bulk that repeats an element an exact number of times.
RepeatNWith
A bulk that repeats elements of type A an exact number of times by applying the provided closure F: FnMut() -> A.
Rev
A double-ended bulk with the direction inverted.
Skip
A bulk that skips over n elements of bulk.
StepBy
A bulk that steps by a custom amount.
Take
A bulk that only delivers the first n iterations of bulk.
Zip
A bulk that operates on two other bulks simultaneously.

Traits§

AsBulk
Bulk
A trait for dealing with bulks.
CollectNearest
CollectionAdapter
CollectionStrategy
DoubleEndedBulk
EitherIntoBulk
EmptyBulk
FromBulk
Conversion from a Bulk.
InplaceBulk
IntoBulk
IntoContainedBy
OnceBulk
RandomAccessBulk
SplitBulk
StaticBulk
A trait for bulks whose length can be determined at compile-time.
Step
Temporary solution because they haven’t made the Step trait const yet… :(
TryCollectionAdapter

Functions§

chain
Converts the arguments to bulks and links them together, in a chain.
empty
Creates a bulk that yields nothing.
once
Creates a bulk that yields an element exactly once.
once_with
Creates a bulk that lazily generates a value exactly once by invoking the provided closure.
repeat_n
Creates a new bulk that repeats a single element a given number of times.
repeat_n_with
Creates a new bulk that repeats elements of type A a given number of times applying the provided closure, the repeater, F: FnMut() -> A.
rzip
Converts the arguments to bulks and zips them.
take
Creates a bulk that only delivers the first n iterations of iterable.
zip
Converts the arguments to bulks and zips them.