Browse Source

Implemented core features for async parallel iterator crate called asparit

master
Bergmann89 3 years ago
parent
commit
5fb9e8f8a2
29 changed files with 1269 additions and 199 deletions
  1. +1
    -0
      .gitignore
  2. +2
    -2
      Cargo.toml
  3. +1
    -1
      asparit/Cargo.toml
  4. +315
    -0
      asparit/src/README.md
  5. +30
    -0
      asparit/src/core/collector.rs
  6. +56
    -0
      asparit/src/core/consumer.rs
  7. +21
    -0
      asparit/src/core/executor.rs
  8. +43
    -0
      asparit/src/core/folder.rs
  9. +148
    -0
      asparit/src/core/into_iter.rs
  10. +156
    -0
      asparit/src/core/iterator.rs
  11. +17
    -0
      asparit/src/core/mod.rs
  12. +144
    -0
      asparit/src/core/producer.rs
  13. +12
    -0
      asparit/src/core/reducer.rs
  14. +5
    -0
      asparit/src/executor/mod.rs
  15. +70
    -0
      asparit/src/executor/sequential.rs
  16. +107
    -0
      asparit/src/inner/for_each.rs
  17. +2
    -0
      asparit/src/inner/mod.rs
  18. +51
    -0
      asparit/src/inner/noop.rs
  19. +11
    -0
      asparit/src/lib.rs
  20. +1
    -0
      asparit/src/std/mod.rs
  21. +71
    -0
      asparit/src/std/range.rs
  22. +1
    -1
      async-ecs/Cargo.toml
  23. +1
    -1
      async-ecs/src/access/par_join.rs
  24. +1
    -1
      async-ecs/src/lib.rs
  25. +1
    -1
      async-ecs/src/misc/bit_producer.rs
  26. +1
    -3
      async-ecs/src/misc/tokio_executor.rs
  27. +0
    -61
      parallel-iterator/src/for_each.rs
  28. +0
    -121
      parallel-iterator/src/lib.rs
  29. +0
    -7
      parallel-iterator/src/no_op.rs

+ 1
- 0
.gitignore View File

@@ -1,2 +1,3 @@
/target
/.vscode
Cargo.lock

+ 2
- 2
Cargo.toml View File

@@ -1,10 +1,10 @@
[workspace]
members = [
"asparit",
"async-ecs",
"async-ecs-derive",
"parallel-iterator"
]

[patch.crates-io]
asparit = { path = "asparit" }
async-ecs-derive = { path = "async-ecs-derive" }
parallel-iterator = { path = "parallel-iterator" }

parallel-iterator/Cargo.toml → asparit/Cargo.toml View File

@@ -1,5 +1,5 @@
[package]
name = "parallel-iterator"
name = "asparit"
version = "0.1.0"
authors = ["Bergmann89 <info@bergmann89.de>"]
edition = "2018"

+ 315
- 0
asparit/src/README.md View File

@@ -0,0 +1,315 @@
# Parallel Iterators

These are some notes on the design of the parallel iterator traits.
This file does not describe how to **use** parallel iterators.

## The challenge

Parallel iterators are more complicated than sequential iterators.
The reason is that they have to be able to split themselves up and
operate in parallel across the two halves.

The current design for parallel iterators has two distinct modes in
which they can be used; as we will see, not all iterators support both
modes (which is why there are two):

- **Pull mode** (the `Producer` and `UnindexedProducer` traits): in this mode,
the iterator is asked to produce the next item using a call to `next`. This
is basically like a normal iterator, but with a twist: you can split the
iterator in half to produce disjoint items in separate threads.
- in the `Producer` trait, splitting is done with `split_at`, which accepts
an index where the split should be performed. Only indexed iterators can
work in this mode, as they know exactly how much data they will produce,
and how to locate the requested index.
- in the `UnindexedProducer` trait, splitting is done with `split`, which
simply requests that the producer divide itself *approximately* in half.
This is useful when the exact length and/or layout is unknown, as with
`String` characters, or when the length might exceed `usize`, as with
`Range<u64>` on 32-bit platforms.
- In theory, any `Producer` could act unindexed, but we don't currently
use that possibility. When you know the exact length, a `split` can
simply be implemented as `split_at(length/2)`.
- **Push mode** (the `Consumer` and `UnindexedConsumer` traits): in
this mode, the iterator instead is *given* each item in turn, which
is then processed. This is the opposite of a normal iterator. It's
more like a `for_each` call: each time a new item is produced, the
`consume` method is called with that item. (The traits themselves are
a bit more complex, as they support state that can be threaded
through and ultimately reduced.) Unlike producers, there are two
variants of consumers. The difference is how the split is performed:
- in the `Consumer` trait, splitting is done with `split_at`, which
accepts an index where the split should be performed. All
iterators can work in this mode. The resulting halves thus have an
idea about how much data they expect to consume.
- in the `UnindexedConsumer` trait, splitting is done with
`split_off_left`. There is no index: the resulting halves must be
prepared to process any amount of data, and they don't know where that
data falls in the overall stream.
- Not all consumers can operate in this mode. It works for
`for_each` and `reduce`, for example, but it does not work for
`collect_into_vec`, since in that case the position of each item is
important for knowing where it ends up in the target collection.

## How iterator execution proceeds

We'll walk through this example iterator chain to start. This chain
demonstrates more-or-less the full complexity of what can happen.

```rust
vec1.par_iter()
.zip(vec2.par_iter())
.flat_map(some_function)
.for_each(some_other_function)
```

To handle an iterator chain, we start by creating consumers. This
works from the end. So in this case, the call to `for_each` is the
final step, so it will create a `ForEachConsumer` that, given an item,
just calls `some_other_function` with that item. (`ForEachConsumer` is
a very simple consumer because it doesn't need to thread any state
between items at all.)

Now, the `for_each` call will pass this consumer to the base iterator,
which is the `flat_map`. It will do this by calling the `drive_unindexed`
method on the `ParallelIterator` trait. `drive_unindexed` basically
says "produce items for this iterator and feed them to this consumer";
it only works for unindexed consumers.

(As an aside, it is interesting that only some consumers can work in
unindexed mode, but all producers can *drive* an unindexed consumer.
In contrast, only some producers can drive an *indexed* consumer, but
all consumers can be supplied indexes. Isn't variance neat.)

As it happens, `FlatMap` only works with unindexed consumers anyway.
This is because flat-map basically has no idea how many items it will
produce. If you ask flat-map to produce the 22nd item, it can't do it,
at least not without some intermediate state. It doesn't know whether
processing the first item will create 1 item, 3 items, or 100;
therefore, to produce an arbitrary item, it would basically just have
to start at the beginning and execute sequentially, which is not what
we want. But for unindexed consumers, this doesn't matter, since they
don't need to know how much data they will get.

Therefore, `FlatMap` can wrap the `ForEachConsumer` with a
`FlatMapConsumer` that feeds to it. This `FlatMapConsumer` will be
given one item. It will then invoke `some_function` to get a parallel
iterator out. It will then ask this new parallel iterator to drive the
`ForEachConsumer`. The `drive_unindexed` method on `flat_map` can then
pass the `FlatMapConsumer` up the chain to the previous item, which is
`zip`. At this point, something interesting happens.

## Switching from push to pull mode

If you think about `zip`, it can't really be implemented as a
consumer, at least not without an intermediate thread and some
channels or something (or maybe coroutines). The problem is that it
has to walk two iterators *in lockstep*. Basically, it can't call two
`drive` methods simultaneously, it can only call one at a time. So at
this point, the `zip` iterator needs to switch from *push mode* into
*pull mode*.

You'll note that `Zip` is only usable if its inputs implement
`IndexedParallelIterator`, meaning that they can produce data starting
at random points in the stream. This need to switch to push mode is
exactly why. If we want to split a zip iterator at position 22, we
need to be able to start zipping items from index 22 right away,
without having to start from index 0.

Anyway, so at this point, the `drive_unindexed` method for `Zip` stops
creating consumers. Instead, it creates a *producer*, a `ZipProducer`,
to be exact, and calls the `bridge` function in the `internals`
module. Creating a `ZipProducer` will in turn create producers for
the two iterators being zipped. This is possible because they both
implement `IndexedParallelIterator`.

The `bridge` function will then connect the consumer, which is
handling the `flat_map` and `for_each`, with the producer, which is
handling the `zip` and its preecessors. It will split down until the
chunks seem reasonably small, then pull items from the producer and
feed them to the consumer.

## The base case

The other time that `bridge` gets used is when we bottom out in an
indexed producer, such as a slice or range. There is also a
`bridge_unindexed` equivalent for - you guessed it - unindexed producers,
such as string characters.

<a name="producer-callback">

## What on earth is `ProducerCallback`?

We saw that when you call a parallel action method like
`par_iter.reduce()`, that will create a "reducing" consumer and then
invoke `par_iter.drive_unindexed()` (or `par_iter.drive()`) as
appropriate. This may create yet more consumers as we proceed up the
parallel iterator chain. But at some point we're going to get to the
start of the chain, or to a parallel iterator (like `zip()`) that has
to coordinate multiple inputs. At that point, we need to start
converting parallel iterators into producers.

The way we do this is by invoking the method `with_producer()`, defined on
`IndexedParallelIterator`. This is a callback scheme. In an ideal world,
it would work like this:

```rust
base_iter.with_producer(|base_producer| {
// here, `base_producer` is the producer for `base_iter`
});
```

In that case, we could implement a combinator like `map()` by getting
the producer for the base iterator, wrapping it to make our own
`MapProducer`, and then passing that to the callback. Something like
this:

```rust
struct MapProducer<'f, P, F: 'f> {
base: P,
map_op: &'f F,
}

impl<I, F> IndexedParallelIterator for Map<I, F>
where I: IndexedParallelIterator,
F: MapOp<I::Item>,
{
fn with_producer<CB>(self, callback: CB) -> CB::Output {
let map_op = &self.map_op;
self.base_iter.with_producer(|base_producer| {
// Here `producer` is the producer for `self.base_iter`.
// Wrap that to make a `MapProducer`
let map_producer = MapProducer {
base: base_producer,
map_op: map_op
};

// invoke the callback with the wrapped version
callback(map_producer)
});
}
});
```

This example demonstrates some of the power of the callback scheme.
It winds up being a very flexible setup. For one thing, it means we
can take ownership of `par_iter`; we can then in turn give ownership
away of its bits and pieces into the producer (this is very useful if
the iterator owns an `&mut` slice, for example), or create shared
references and put *those* in the producer. In the case of map, for
example, the parallel iterator owns the `map_op`, and we borrow
references to it which we then put into the `MapProducer` (this means
the `MapProducer` can easily split itself and share those references).
The `with_producer` method can also create resources that are needed
during the parallel execution, since the producer does not have to be
returned.

Unfortunately there is a catch. We can't actually use closures the way
I showed you. To see why, think about the type that `map_producer`
would have to have. If we were going to write the `with_producer`
method using a closure, it would have to look something like this:

```rust
pub trait IndexedParallelIterator: ParallelIterator {
type Producer;
fn with_producer<CB, R>(self, callback: CB) -> R
where CB: FnOnce(Self::Producer) -> R;
...
}
```

Note that we had to add this associated type `Producer` so that
we could specify the argument of the callback to be `Self::Producer`.
Now, imagine trying to write that `MapProducer` impl using this style:

```rust
impl<I, F> IndexedParallelIterator for Map<I, F>
where I: IndexedParallelIterator,
F: MapOp<I::Item>,
{
type MapProducer = MapProducer<'f, P::Producer, F>;
// ^^ wait, what is this `'f`?

fn with_producer<CB, R>(self, callback: CB) -> R
where CB: FnOnce(Self::Producer) -> R
{
let map_op = &self.map_op;
// ^^^^^^ `'f` is (conceptually) the lifetime of this reference,
// so it will be different for each call to `with_producer`!
}
}
```

This may look familiar to you: it's the same problem that we have
trying to define an `Iterable` trait. Basically, the producer type
needs to include a lifetime (here, `'f`) that refers to the body of
`with_producer` and hence is not in scope at the impl level.

If we had [associated type constructors][1598], we could solve this
problem that way. But there is another solution. We can use a
dedicated callback trait like `ProducerCallback`, instead of `FnOnce`:

[1598]: https://github.com/rust-lang/rfcs/pull/1598

```rust
pub trait ProducerCallback<T> {
type Output;
fn callback<P>(self, producer: P) -> Self::Output
where P: Producer<Item=T>;
}
```

Using this trait, the signature of `with_producer()` looks like this:

```rust
fn with_producer<CB: ProducerCallback<Self::Item>>(self, callback: CB) -> CB::Output;
```

Notice that this signature **never has to name the producer type** --
there is no associated type `Producer` anymore. This is because the
`callback()` method is generically over **all** producers `P`.

The problem is that now the `||` sugar doesn't work anymore. So we
have to manually create the callback struct, which is a mite tedious.
So our `MapProducer` code looks like this:

```rust
impl<I, F> IndexedParallelIterator for Map<I, F>
where I: IndexedParallelIterator,
F: MapOp<I::Item>,
{
fn with_producer<CB>(self, callback: CB) -> CB::Output
where CB: ProducerCallback<Self::Item>
{
return self.base.with_producer(Callback { callback: callback, map_op: self.map_op });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Manual version of the closure sugar: create an instance
// of a struct that implements `ProducerCallback`.

// The struct declaration. Each field is something that need to capture from the
// creating scope.
struct Callback<CB, F> {
callback: CB,
map_op: F,
}

// Implement the `ProducerCallback` trait. This is pure boilerplate.
impl<T, F, CB> ProducerCallback<T> for Callback<CB, F>
where F: MapOp<T>,
CB: ProducerCallback<F::Output>
{
type Output = CB::Output;

fn callback<P>(self, base: P) -> CB::Output
where P: Producer<Item=T>
{
// The body of the closure is here:
let producer = MapProducer { base: base,
map_op: &self.map_op };
self.callback.callback(producer)
}
}
}
}
```

OK, a bit tedious, but it works!

+ 30
- 0
asparit/src/core/collector.rs View File

@@ -0,0 +1,30 @@
use crate::{
Consumer, DefaultExecutor, Executor, IndexedConsumer, IndexedExecutor, IndexedParallelIterator,
ParallelIterator,
};

pub trait Collector: Sized {
type Iterator: ParallelIterator;
type Consumer: Consumer<<Self::Iterator as ParallelIterator>::Item>;

fn exec_with<E>(self, executor: E) -> E::Result
where
E: Executor<Self::Iterator, Self::Consumer>;

fn exec(self) -> <DefaultExecutor as Executor<Self::Iterator, Self::Consumer>>::Result {
self.exec_with(DefaultExecutor::default())
}
}

pub trait IndexedCollector: Sized {
type Iterator: IndexedParallelIterator;
type Consumer: IndexedConsumer<<Self::Iterator as ParallelIterator>::Item>;

fn exec_with<E>(self, executor: E) -> E::Result
where
E: IndexedExecutor<Self::Iterator, Self::Consumer>;

fn exec(self) -> <DefaultExecutor as Executor<Self::Iterator, Self::Consumer>>::Result {
self.exec_with(DefaultExecutor::default())
}
}

+ 56
- 0
asparit/src/core/consumer.rs View File

@@ -0,0 +1,56 @@
use super::{Folder, Reducer};

/// A consumer is effectively a [generalized "fold" operation][fold],
/// and in fact each consumer will eventually be converted into a
/// [`Folder`]. What makes a consumer special is that, like a
/// [`Producer`], it can be **split** into multiple consumers using
/// the `split_off_left` method. When a consumer is split, it produces two
/// consumers, as well as a **reducer**. The two consumers can be fed
/// items independently, and when they are done the reducer is used to
/// combine their two results into one. See [the README][r] for further
/// details.
///
/// [r]: README.md
/// [fold]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold
/// [`Folder`]: trait.Folder.html
/// [`Producer`]: trait.Producer.html
pub trait Consumer<Item>: Send + Sized {
/// The type of folder that this consumer can be converted into.
type Folder: Folder<Item, Result = Self::Result>;

/// The type of reducer that is produced if this consumer is split.
type Reducer: Reducer<Self::Result>;

/// The type of result that this consumer will ultimately produce.
type Result: Send;

/// Splits off a "left" consumer and returns it. The `self`
/// consumer should then be used to consume the "right" portion of
/// the data. (The ordering matters for methods like find_first --
/// values produced by the returned value are given precedence
/// over values produced by `self`.) Once the left and right
/// halves have been fully consumed, you should reduce the results
/// with the result of `to_reducer`.
fn split_off_left(&self) -> (Self, Self::Reducer);

/// Convert the consumer into a folder that can consume items
/// sequentially, eventually producing a final result.
fn into_folder(self) -> Self::Folder;

/// Hint whether this `Consumer` would like to stop processing
/// further items, e.g. if a search has been completed.
fn is_full(&self) -> bool {
false
}
}

/// A stateless consumer can be freely copied. These consumers can be
/// used like regular consumers, but they also support a
/// `split_at` method that does take an index to split.
pub trait IndexedConsumer<Item>: Consumer<Item> {
/// Divide the consumer into two consumers, one processing items
/// `0..index` and one processing items from `index..`. Also
/// produces a reducer that can be used to reduce the results at
/// the end.
fn split_at(self, index: usize) -> (Self, Self, Self::Reducer);
}

+ 21
- 0
asparit/src/core/executor.rs View File

@@ -0,0 +1,21 @@
use super::{Consumer, IndexedConsumer, IndexedParallelIterator, ParallelIterator};

pub trait Executor<I, C>
where
I: ParallelIterator,
C: Consumer<I::Item>,
{
type Result;

fn exec(self, iterator: I, consumer: C) -> Self::Result;
}

pub trait IndexedExecutor<I, C>
where
I: IndexedParallelIterator,
C: IndexedConsumer<I::Item>,
{
type Result;

fn exec_indexed(self, iterator: I, consumer: C) -> Self::Result;
}

+ 43
- 0
asparit/src/core/folder.rs View File

@@ -0,0 +1,43 @@
/// The `Folder` trait encapsulates [the standard fold
/// operation][fold]. It can be fed many items using the `consume`
/// method. At the end, once all items have been consumed, it can then
/// be converted (using `complete`) into a final value.
///
/// [fold]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold
pub trait Folder<Item>: Sized {
/// The type of result that will ultimately be produced by the folder.
type Result;

/// Consume next item and return new sequential state.
fn consume(self, item: Item) -> Self;

/// Consume items from the iterator until full, and return new sequential state.
///
/// This method is **optional**. The default simply iterates over
/// `iter`, invoking `consume` and checking after each iteration
/// whether `full` returns false.
///
/// The main reason to override it is if you can provide a more
/// specialized, efficient implementation.
fn consume_iter<I>(mut self, iter: I) -> Self
where
I: IntoIterator<Item = Item>,
{
for item in iter {
self = self.consume(item);
if self.is_full() {
break;
}
}
self
}

/// Finish consuming items, produce final result.
fn complete(self) -> Self::Result;

/// Hint whether this `Folder` would like to stop processing
/// further items, e.g. if a search has been completed.
fn is_full(&self) -> bool {
false
}
}

+ 148
- 0
asparit/src/core/into_iter.rs View File

@@ -0,0 +1,148 @@
use super::ParallelIterator;

/// `IntoParallelIterator` implements the conversion to a [`ParallelIterator`].
///
/// By implementing `IntoParallelIterator` for a type, you define how it will
/// transformed into an iterator. This is a parallel version of the standard
/// library's [`std::iter::IntoIterator`] trait.
///
/// [`ParallelIterator`]: trait.ParallelIterator.html
/// [`std::iter::IntoIterator`]: https://doc.rust-lang.org/std/iter/trait.IntoIterator.html
pub trait IntoParallelIterator {
/// The parallel iterator type that will be created.
type Iter: ParallelIterator<Item = Self::Item>;

/// The type of item that the parallel iterator will produce.
type Item: Send;

/// Converts `self` into a parallel iterator.
///
/// # Examples
///
/// ```
/// use asparit::*;
///
/// println!("counting in parallel:");
/// (0..100).into_par_iter()
/// .for_each(|i| println!("{}", i));
/// ```
///
/// This conversion is often implicit for arguments to methods like [`zip`].
///
/// ```
/// use asparit::*;
///
/// let v: Vec<_> = (0..5).into_par_iter().zip(5..10).collect();
/// assert_eq!(v, [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]);
/// ```
///
/// [`zip`]: trait.IndexedParallelIterator.html#method.zip
fn into_par_iter(self) -> Self::Iter;
}

/// `IntoParallelRefIterator` implements the conversion to a
/// [`ParallelIterator`], providing shared references to the data.
///
/// This is a parallel version of the `iter()` method
/// defined by various collections.
///
/// This trait is automatically implemented
/// `for I where &I: IntoParallelIterator`. In most cases, users
/// will want to implement [`IntoParallelIterator`] rather than implement
/// this trait directly.
///
/// [`ParallelIterator`]: trait.ParallelIterator.html
/// [`IntoParallelIterator`]: trait.IntoParallelIterator.html
pub trait IntoParallelRefIterator<'data> {
/// The type of the parallel iterator that will be returned.
type Iter: ParallelIterator<Item = Self::Item>;

/// The type of item that the parallel iterator will produce.
/// This will typically be an `&'data T` reference type.
type Item: Send + 'data;

/// Converts `self` into a parallel iterator.
///
/// # Examples
///
/// ```
/// use asparit::*;
///
/// let v: Vec<_> = (0..100).collect();
/// assert_eq!(v.par_iter().sum::<i32>(), 100 * 99 / 2);
///
/// // `v.par_iter()` is shorthand for `(&v).into_par_iter()`,
/// // producing the exact same references.
/// assert!(v.par_iter().zip(&v)
/// .all(|(a, b)| std::ptr::eq(a, b)));
/// ```
fn par_iter(&'data self) -> Self::Iter;
}

/// `IntoParallelRefMutIterator` implements the conversion to a
/// [`ParallelIterator`], providing mutable references to the data.
///
/// This is a parallel version of the `iter_mut()` method
/// defined by various collections.
///
/// This trait is automatically implemented
/// `for I where &mut I: IntoParallelIterator`. In most cases, users
/// will want to implement [`IntoParallelIterator`] rather than implement
/// this trait directly.
///
/// [`ParallelIterator`]: trait.ParallelIterator.html
/// [`IntoParallelIterator`]: trait.IntoParallelIterator.html
pub trait IntoParallelRefMutIterator<'data> {
/// The type of iterator that will be created.
type Iter: ParallelIterator<Item = Self::Item>;

/// The type of item that will be produced; this is typically an
/// `&'data mut T` reference.
type Item: Send + 'data;

/// Creates the parallel iterator from `self`.
///
/// # Examples
///
/// ```
/// use asparit::*;
///
/// let mut v = vec![0usize; 5];
/// v.par_iter_mut().enumerate().for_each(|(i, x)| *x = i);
/// assert_eq!(v, [0, 1, 2, 3, 4]);
/// ```
fn par_iter_mut(&'data mut self) -> Self::Iter;
}

impl<T: ParallelIterator> IntoParallelIterator for T {
type Iter = T;
type Item = T::Item;

fn into_par_iter(self) -> T {
self
}
}

impl<'data, I: 'data + ?Sized> IntoParallelRefIterator<'data> for I
where
&'data I: IntoParallelIterator,
{
type Iter = <&'data I as IntoParallelIterator>::Iter;
type Item = <&'data I as IntoParallelIterator>::Item;

fn par_iter(&'data self) -> Self::Iter {
self.into_par_iter()
}
}

impl<'data, I: 'data + ?Sized> IntoParallelRefMutIterator<'data> for I
where
&'data mut I: IntoParallelIterator,
{
type Iter = <&'data mut I as IntoParallelIterator>::Iter;
type Item = <&'data mut I as IntoParallelIterator>::Item;

fn par_iter_mut(&'data mut self) -> Self::Iter {
self.into_par_iter()
}
}

+ 156
- 0
asparit/src/core/iterator.rs View File

@@ -0,0 +1,156 @@
use super::{Consumer, Executor, IndexedConsumer, IndexedProducerCallback, ProducerCallback};

use crate::inner::for_each::ForEach;

/// Parallel version of the standard iterator trait.
///
/// The combinators on this trait are available on **all** parallel
/// iterators. Additional methods can be found on the
/// [`IndexedParallelIterator`] trait: those methods are only
/// available for parallel iterators where the number of items is
/// known in advance (so, e.g., after invoking `filter`, those methods
/// become unavailable).
///
/// For examples of using parallel iterators, see [the docs on the
/// `iter` module][iter].
///
/// [iter]: index.html
/// [`IndexedParallelIterator`]: trait.IndexedParallelIterator.html
pub trait ParallelIterator: Sized + Send {
/// The type of item that this parallel iterator produces.
/// For example, if you use the [`for_each`] method, this is the type of
/// item that your closure will be invoked with.
///
/// [`for_each`]: #method.for_each
type Item: Send;

/// Internal method used to define the behavior of this parallel
/// iterator. You should not need to call this directly.
///
/// This method causes the iterator `self` to start producing
/// items and to feed them to the consumer `consumer` one by one.
/// It may split the consumer before doing so to create the
/// opportunity to produce in parallel.
///
/// See the [README] for more details on the internals of parallel
/// iterators.
///
/// [README]: README.md
fn drive<E, C>(self, executor: E, consumer: C) -> E::Result
where
E: Executor<Self, C>,
C: Consumer<Self::Item>;

/// Internal method used to define the behavior of this parallel
/// iterator. You should not need to call this directly.
///
/// This method converts the iterator into a producer P and then
/// invokes `callback.callback()` with P. Note that the type of
/// this producer is not defined as part of the API, since
/// `callback` must be defined generically for all producers. This
/// allows the producer type to contain references; it also means
/// that parallel iterators can adjust that type without causing a
/// breaking change.
///
/// See the [README] for more details on the internals of parallel
/// iterators.
///
/// [README]: README.md
fn with_producer<CB>(self, callback: CB) -> CB::Output
where
CB: ProducerCallback<Self::Item>;

/// Internal method used to define the behavior of this parallel
/// iterator. You should not need to call this directly.
///
/// Returns the number of items produced by this iterator, if known
/// statically. This can be used by consumers to trigger special fast
/// paths. Therefore, if `Some(_)` is returned, this iterator must only
/// use the (indexed) `Consumer` methods when driving a consumer, such
/// as `split_at()`. Calling `UnindexedConsumer::split_off_left()` or
/// other `UnindexedConsumer` methods -- or returning an inaccurate
/// value -- may result in panics.
///
/// This method is currently used to optimize `collect` for want
/// of true Rust specialization; it may be removed when
/// specialization is stable.
fn len_hint_opt(&self) -> Option<usize> {
None
}

/// Executes `OP` on each item produced by the iterator, in parallel.
///
/// # Examples
///
/// ```
/// use asparit::*;
///
/// (0..100).into_par_iter().for_each(|x| println!("{:?}", x));
/// ```
fn for_each<F>(self, operation: F) -> ForEach<Self, F>
where
F: Fn(Self::Item) + Sync + Send,
{
ForEach::new(self, operation)
}
}

/// An iterator that supports "random access" to its data, meaning
/// that you can split it at arbitrary indices and draw data from
/// those points.
///
/// **Note:** Not implemented for `u64`, `i64`, `u128`, or `i128` ranges
pub trait IndexedParallelIterator: ParallelIterator {
/// Produces an exact count of how many items this iterator will
/// produce, presuming no panic occurs.
///
/// # Examples
///
/// ```
/// use asparit::*;
///
/// let par_iter = (0..100).into_par_iter().zip(vec![0; 10]);
/// assert_eq!(par_iter.len(), 10);
///
/// let vec: Vec<_> = par_iter.collect();
/// assert_eq!(vec.len(), 10);
/// ```
fn len_hint(&self) -> usize;

/// Internal method used to define the behavior of this parallel
/// iterator. You should not need to call this directly.
///
/// This method causes the iterator `self` to start producing
/// items and to feed them to the consumer `consumer` one by one.
/// It may split the consumer before doing so to create the
/// opportunity to produce in parallel. If a split does happen, it
/// will inform the consumer of the index where the split should
/// occur (unlike `ParallelIterator::drive_unindexed()`).
///
/// See the [README] for more details on the internals of parallel
/// iterators.
///
/// [README]: README.md
fn drive_indexed<C>(self, consumer: C) -> C::Result
where
C: IndexedConsumer<Self::Item>;

/// Internal method used to define the behavior of this parallel
/// iterator. You should not need to call this directly.
///
/// This method converts the iterator into a producer P and then
/// invokes `callback.callback()` with P. Note that the type of
/// this producer is not defined as part of the API, since
/// `callback` must be defined generically for all producers. This
/// allows the producer type to contain references; it also means
/// that parallel iterators can adjust that type without causing a
/// breaking change.
///
/// See the [README] for more details on the internals of parallel
/// iterators.
///
/// [README]: README.md
fn with_producer_indexed<CB>(self, callback: CB) -> CB::Output
where
CB: IndexedProducerCallback<Self::Item>;
}

+ 17
- 0
asparit/src/core/mod.rs View File

@@ -0,0 +1,17 @@
mod collector;
mod consumer;
mod executor;
mod folder;
mod into_iter;
mod iterator;
mod producer;
mod reducer;

pub use collector::Collector;
pub use consumer::{Consumer, IndexedConsumer};
pub use executor::{Executor, IndexedExecutor};
pub use folder::Folder;
pub use into_iter::{IntoParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator};
pub use iterator::{IndexedParallelIterator, ParallelIterator};
pub use producer::{IndexedProducer, IndexedProducerCallback, Producer, ProducerCallback};
pub use reducer::Reducer;

+ 144
- 0
asparit/src/core/producer.rs View File

@@ -0,0 +1,144 @@
use super::Folder;

/// A variant on `Producer` which does not know its exact length or
/// cannot represent it in a `usize`. These producers act like
/// ordinary producers except that they cannot be told to split at a
/// particular point. Instead, you just ask them to split 'somewhere'.
///
/// (In principle, `Producer` could extend this trait; however, it
/// does not because to do so would require producers to carry their
/// own length with them.)
pub trait Producer: Send + Sized {
/// The type of item returned by this producer.
type Item;

/// Split midway into a new producer if possible, otherwise return `None`.
fn split(self) -> (Self, Option<Self>);

/// Iterate the producer, feeding each element to `folder`, and
/// stop when the folder is full (or all elements have been consumed).
fn fold_with<F>(self, folder: F) -> F
where
F: Folder<Self::Item>;
}

/// A `Producer` is effectively a "splittable `IntoIterator`". That
/// is, a producer is a value which can be converted into an iterator
/// at any time: at that point, it simply produces items on demand,
/// like any iterator. But what makes a `Producer` special is that,
/// *before* we convert to an iterator, we can also **split** it at a
/// particular point using the `split_at` method. This will yield up
/// two producers, one producing the items before that point, and one
/// producing the items after that point (these two producers can then
/// independently be split further, or be converted into iterators).
/// In Rayon, this splitting is used to divide between threads.
/// See [the `plumbing` README][r] for further details.
///
/// Note that each producer will always produce a fixed number of
/// items N. However, this number N is not queryable through the API;
/// the consumer is expected to track it.
///
/// NB. You might expect `Producer` to extend the `IntoIterator`
/// trait. However, [rust-lang/rust#20671][20671] prevents us from
/// declaring the DoubleEndedIterator and ExactSizeIterator
/// constraints on a required IntoIterator trait, so we inline
/// IntoIterator here until that issue is fixed.
///
/// [r]: https://github.com/rayon-rs/rayon/blob/master/src/iter/plumbing/README.md
/// [20671]: https://github.com/rust-lang/rust/issues/20671
pub trait IndexedProducer: Send + Sized {
/// The type of item that will be produced by this producer once
/// it is converted into an iterator.
type Item;

/// The type of iterator we will become.
type IntoIter: Iterator<Item = Self::Item> + DoubleEndedIterator + ExactSizeIterator;

/// Convert `self` into an iterator; at this point, no more parallel splits
/// are possible.
fn into_iter(self) -> Self::IntoIter;

/// The minimum number of items that we will process
/// sequentially. Defaults to 1, which means that we will split
/// all the way down to a single item. This can be raised higher
/// using the [`with_min_len`] method, which will force us to
/// create sequential tasks at a larger granularity. Note that
/// Rayon automatically normally attempts to adjust the size of
/// parallel splits to reduce overhead, so this should not be
/// needed.
///
/// [`with_min_len`]: ../trait.IndexedParallelIterator.html#method.with_min_len
fn min_len(&self) -> usize {
1
}

/// The maximum number of items that we will process
/// sequentially. Defaults to MAX, which means that we can choose
/// not to split at all. This can be lowered using the
/// [`with_max_len`] method, which will force us to create more
/// parallel tasks. Note that Rayon automatically normally
/// attempts to adjust the size of parallel splits to reduce
/// overhead, so this should not be needed.
///
/// [`with_max_len`]: ../trait.IndexedParallelIterator.html#method.with_max_len
fn max_len(&self) -> usize {
usize::MAX
}

/// Split into two producers; one produces items `0..index`, the
/// other `index..N`. Index must be less than or equal to `N`.
fn split_at(self, index: usize) -> (Self, Self);

/// Iterate the producer, feeding each element to `folder`, and
/// stop when the folder is full (or all elements have been consumed).
///
/// The provided implementation is sufficient for most iterables.
fn fold_with<F>(self, folder: F) -> F
where
F: Folder<Self::Item>,
{
folder.consume_iter(self.into_iter())
}
}

/// The `ProducerCallback` trait is a kind of generic closure,
/// [analogous to `FnOnce`][FnOnce]. See [the corresponding section in
/// the plumbing README][r] for more details.
///
/// [r]: https://github.com/rayon-rs/rayon/blob/master/src/iter/plumbing/README.md#producer-callback
/// [FnOnce]: https://doc.rust-lang.org/std/ops/trait.FnOnce.html
pub trait ProducerCallback<T> {
/// The type of value returned by this callback. Analogous to
/// [`Output` from the `FnOnce` trait][Output].
///
/// [Output]: https://doc.rust-lang.org/std/ops/trait.FnOnce.html#associatedtype.Output
type Output;

/// Invokes the callback with the given producer as argument. The
/// key point of this trait is that this method is generic over
/// `P`, and hence implementors must be defined for any producer.
fn callback<P>(self, producer: P) -> Self::Output
where
P: Producer<Item = T>;
}

/// The `IndexedProducerCallback` trait is a kind of generic closure,
/// [analogous to `FnOnce`][FnOnce]. See [the corresponding section in
/// the plumbing README][r] for more details.
///
/// [r]: https://github.com/rayon-rs/rayon/blob/master/src/iter/plumbing/README.md#producer-callback
/// [FnOnce]: https://doc.rust-lang.org/std/ops/trait.FnOnce.html
pub trait IndexedProducerCallback<T> {
/// The type of value returned by this callback. Analogous to
/// [`Output` from the `FnOnce` trait][Output].
///
/// [Output]: https://doc.rust-lang.org/std/ops/trait.FnOnce.html#associatedtype.Output
type Output;

/// Invokes the callback with the given producer as argument. The
/// key point of this trait is that this method is generic over
/// `P`, and hence implementors must be defined for any producer.
fn callback<P>(self, producer: P) -> Self::Output
where
P: IndexedProducer<Item = T>;
}

+ 12
- 0
asparit/src/core/reducer.rs View File

@@ -0,0 +1,12 @@
/// The reducer is the final step of a `Consumer` -- after a consumer
/// has been split into two parts, and each of those parts has been
/// fully processed, we are left with two results. The reducer is then
/// used to combine those two results into one. See [the `plumbing`
/// README][r] for further details.
///
/// [r]: https://github.com/rayon-rs/rayon/blob/master/src/iter/plumbing/README.md
pub trait Reducer<Result> {
/// Reduce two final results into one; this is executed after a
/// split.
fn reduce(self, left: Result, right: Result) -> Result;
}

+ 5
- 0
asparit/src/executor/mod.rs View File

@@ -0,0 +1,5 @@
mod sequential;

pub use sequential::Sequential as SequentialExecutor;

pub type DefaultExecutor = SequentialExecutor;

+ 70
- 0
asparit/src/executor/sequential.rs View File

@@ -0,0 +1,70 @@
use crate::core::{
Consumer, Executor, Folder, IndexedConsumer, IndexedExecutor, IndexedParallelIterator,
IndexedProducer, IndexedProducerCallback, ParallelIterator, Producer, ProducerCallback,
};

#[derive(Default)]
pub struct Sequential;

struct Callback<C> {
consumer: C,
}

struct IndexedCallback<C> {
consumer: C,
}

impl<I, C> Executor<I, C> for Sequential
where
I: ParallelIterator,
C: Consumer<I::Item>,
{
type Result = C::Result;

fn exec(self, iterator: I, consumer: C) -> Self::Result {
iterator.with_producer(Callback { consumer })
}
}

impl<I, C> IndexedExecutor<I, C> for Sequential
where
I: IndexedParallelIterator,
C: IndexedConsumer<I::Item>,
{
type Result = C::Result;

fn exec_indexed(self, iterator: I, consumer: C) -> Self::Result {
iterator.with_producer_indexed(IndexedCallback { consumer })
}
}

impl<C, T> ProducerCallback<T> for Callback<C>
where
C: Consumer<T>,
{
type Output = C::Result;

fn callback<P>(self, producer: P) -> C::Result
where
P: Producer<Item = T>,
{
if self.consumer.is_full() {
self.consumer.into_folder().complete()
} else {
producer.fold_with(self.consumer.into_folder()).complete()
}
}
}

impl<C, T> IndexedProducerCallback<T> for IndexedCallback<C>
where
C: IndexedConsumer<T>,
{
type Output = C::Result;
fn callback<P>(self, _producer: P) -> C::Result
where
P: IndexedProducer<Item = T>,
{
self.consumer.into_folder().complete()
}
}

+ 107
- 0
asparit/src/inner/for_each.rs View File

@@ -0,0 +1,107 @@
use crate::{
core::{Collector, Executor},
Consumer, Folder, ParallelIterator,
};

use super::noop::NoOpReducer;

pub struct ForEach<I, F> {
iterator: I,
operation: F,
}

impl<I, F> ForEach<I, F> {
pub fn new(iterator: I, operation: F) -> Self {
Self {
iterator,
operation,
}
}
}

impl<I, F> Collector for ForEach<I, F>
where
I: ParallelIterator,
F: Fn(I::Item) + Sync + Send + Copy,
{
type Iterator = I;
type Consumer = ForEachConsumer<F>;

fn exec_with<E>(self, executor: E) -> E::Result
where
E: Executor<Self::Iterator, Self::Consumer>,
{
let iterator = self.iterator;
let operation = self.operation;

let consumer = ForEachConsumer { operation };

iterator.drive(executor, consumer)
}
}

pub struct ForEachConsumer<F> {
operation: F,
}

impl<F, T> Consumer<T> for ForEachConsumer<F>
where
F: Fn(T) + Sync + Send + Copy,
{
type Folder = ForEachConsumer<F>;
type Reducer = NoOpReducer;
type Result = ();

fn split_off_left(&self) -> (Self, NoOpReducer) {
(
ForEachConsumer {
operation: self.operation,
},
NoOpReducer,
)
}

fn into_folder(self) -> Self {
self
}
}

impl<F, T> Folder<T> for ForEachConsumer<F>
where
F: Fn(T) + Sync + Send + Copy,
{
type Result = ();

fn consume(self, item: T) -> Self {
(self.operation)(item);

self
}

fn consume_iter<I>(self, iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
iter.into_iter().for_each(self.operation);

self
}

fn complete(self) {}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::*;

#[test]
fn test_for_each() {
(0..10usize)
.into_par_iter()
.for_each(&|j| {
println!("{}", j);
})
.exec();
}
}

+ 2
- 0
asparit/src/inner/mod.rs View File

@@ -0,0 +1,2 @@
pub mod for_each;
pub mod noop;

+ 51
- 0
asparit/src/inner/noop.rs View File

@@ -0,0 +1,51 @@
use crate::{Consumer, Folder, IndexedConsumer, Reducer};

pub struct NoOpConsumer;

impl<T> Consumer<T> for NoOpConsumer {
type Folder = NoOpConsumer;
type Reducer = NoOpReducer;
type Result = ();

fn split_off_left(&self) -> (Self, Self::Reducer) {
(NoOpConsumer, NoOpReducer)
}

fn into_folder(self) -> Self {
self
}

fn is_full(&self) -> bool {
false
}
}

impl<T> Folder<T> for NoOpConsumer {
type Result = ();

fn consume(self, _item: T) -> Self {
self
}

fn consume_iter<I>(self, iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
iter.into_iter().for_each(drop);
self
}

fn complete(self) {}
}

impl<T> IndexedConsumer<T> for NoOpConsumer {
fn split_at(self, _index: usize) -> (Self, Self, Self::Reducer) {
(NoOpConsumer, NoOpConsumer, NoOpReducer)
}
}

pub struct NoOpReducer;

impl Reducer<()> for NoOpReducer {
fn reduce(self, _left: (), _right: ()) {}
}

+ 11
- 0
asparit/src/lib.rs View File

@@ -0,0 +1,11 @@
mod core;
mod executor;
mod inner;
mod std;

pub use self::core::{
Consumer, Executor, Folder, IndexedConsumer, IndexedExecutor, IndexedParallelIterator,
IndexedProducer, IntoParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator,
ParallelIterator, Producer, ProducerCallback, Reducer,
};
pub use self::executor::{DefaultExecutor, SequentialExecutor};

+ 1
- 0
asparit/src/std/mod.rs View File

@@ -0,0 +1 @@
mod range;

+ 71
- 0
asparit/src/std/range.rs View File

@@ -0,0 +1,71 @@
use std::ops::Range;

use crate::{
Consumer, Executor, Folder, IntoParallelIterator, ParallelIterator, Producer, ProducerCallback,
};

pub struct Iter {
range: Range<usize>,
}

struct IterProducer {
range: Range<usize>,
}

impl IntoParallelIterator for Range<usize> {
type Iter = Iter;
type Item = usize;

fn into_par_iter(self) -> Self::Iter {
Iter { range: self }
}
}

impl ParallelIterator for Iter {
type Item = usize;

fn drive<E, C>(self, executor: E, consumer: C) -> E::Result
where
E: Executor<Self, C>,
C: Consumer<Self::Item>,
{
executor.exec(self, consumer)
}

fn len_hint_opt(&self) -> Option<usize> {
Some(self.range.end - self.range.start)
}

fn with_producer<CB>(self, callback: CB) -> CB::Output
where
CB: ProducerCallback<Self::Item>,
{
callback.callback(IterProducer { range: self.range })
}
}

impl Producer for IterProducer {
type Item = usize;

fn split(mut self) -> (Self, Option<Self>) {
let index = self.range.len() / 2;

if index > 0 {
let mid = self.range.start.wrapping_add(index);
let right = mid..self.range.end;

self.range.end = mid;

(self, Some(IterProducer { range: right }))
} else {
(self, None)
}
}

fn fold_with<F>(self, folder: F) -> F
where
F: Folder<Self::Item>,
{
folder.consume_iter(self.range)
}
}

+ 1
- 1
async-ecs/Cargo.toml View File

@@ -5,6 +5,7 @@ authors = ["Bergmann89 <info@bergmann89.de>"]
edition = "2018"

[dependencies]
asparit = "0.1"
async-ecs-derive = "0.1"
env_logger = "0.8"
futures = "0.3"
@@ -13,7 +14,6 @@ hibitset = { version = "0.6", default-features = false }
log = "0.4"
mopa = "0.2"
num_cpus = "1.13"
parallel-iterator = "0.1"
rand = "0.7"
thiserror = "1.0"
tokio = { version = "0.3", features = [ "full", "net", "time", "rt-multi-thread" ] }

+ 1
- 1
async-ecs/src/access/par_join.rs View File

@@ -1,7 +1,7 @@
use std::cell::UnsafeCell;

use asparit::{Consumer, Executor, Folder, ParallelIterator, Producer};
use hibitset::BitSetLike;
use parallel_iterator::{Consumer, Executor, Folder, ParallelIterator, Producer};

use crate::misc::{BitIter, BitProducer, TokioExecutor};



+ 1
- 1
async-ecs/src/lib.rs View File

@@ -11,7 +11,7 @@ pub mod storage;
pub mod system;
pub mod world;

pub use parallel_iterator::ParallelIterator;
pub use asparit::ParallelIterator;

pub use access::{Join, ParJoin, ReadStorage, WriteStorage};
pub use dispatcher::Dispatcher;


+ 1
- 1
async-ecs/src/misc/bit_producer.rs View File

@@ -1,5 +1,5 @@
use asparit::{Folder, Producer};
use hibitset::BitSetLike;
use parallel_iterator::{Folder, Producer};

use crate::misc::bit_average;



+ 1
- 3
async-ecs/src/misc/tokio_executor.rs View File

@@ -7,9 +7,7 @@ use futures::{
};
use tokio::task::{block_in_place, spawn};

use parallel_iterator::{
Consumer, Executor, Folder, IndexedConsumer, IndexedProducer, Producer, Reducer,
};
use asparit::{Consumer, Executor, Folder, IndexedConsumer, IndexedProducer, Producer, Reducer};

pub struct TokioExecutor;



+ 0
- 61
parallel-iterator/src/for_each.rs View File

@@ -1,61 +0,0 @@
use super::{no_op::NoOpReducer, Consumer, Folder, ParallelIterator};

pub fn for_each<I, F, T>(iter: I, f: &F)
where
I: ParallelIterator<Item = T>,
F: Fn(T) + Sync,
T: Send,
{
let consumer = ForEachConsumer { f };

iter.drive(consumer)
}

struct ForEachConsumer<'f, F> {
f: &'f F,
}

impl<'f, F, T> Consumer<T> for ForEachConsumer<'f, F>
where
F: Fn(T) + Sync,
{
type Folder = ForEachConsumer<'f, F>;
type Reducer = NoOpReducer;
type Result = ();

fn split_off_left(&self) -> (Self, Self::Reducer) {
(Self { f: self.f }, NoOpReducer)
}

fn into_folder(self) -> Self::Folder {
self
}
}

impl<'f, F, T> Folder<T> for ForEachConsumer<'f, F>
where
F: Fn(T) + Sync,
{
type Result = ();

fn is_full(&self) -> bool {
false
}

fn complete(self) -> Self::Result {}

fn consume(self, item: T) -> Self {
(self.f)(item);

self
}

fn consume_iter<I>(self, iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
iter.into_iter().for_each(self.f);

self
}
}

+ 0
- 121
parallel-iterator/src/lib.rs View File

@@ -1,121 +0,0 @@
mod for_each;
mod no_op;

pub trait ParallelIterator: Sized + Send {
type Item: Send;

fn for_each<F>(self, f: F)
where
F: Fn(Self::Item) + Sync + Send,
{
for_each::for_each(self, &f)
}

fn drive<C>(self, consumer: C) -> C::Result
where
C: Consumer<Self::Item>;

fn len_hint_opt(&self) -> Option<usize> {
None
}
}

pub trait IndexedParallelIterator: ParallelIterator {
fn drive_indexed<C>(self, consumer: C) -> C::Result
where
C: IndexedConsumer<Self::Item>;

fn len_hint(&self) -> usize;
}

pub trait Producer: Send + Sized {
type Item;

fn split(self) -> (Self, Option<Self>);

fn fold_with<F>(self, folder: F) -> F
where
F: Folder<Self::Item>;
}

pub trait IndexedProducer: Send + Sized {
type Item;
type IntoIter: Iterator<Item = Self::Item> + DoubleEndedIterator + ExactSizeIterator;

fn into_iter(self) -> Self::IntoIter;

fn min_len(&self) -> usize {
1
}

fn max_len(&self) -> usize {
usize::MAX
}

fn split_at(self, index: usize) -> (Self, Self);

fn fold_with<F>(self, folder: F) -> F
where
F: Folder<Self::Item>,
{
folder.consume_iter(self.into_iter())
}
}

pub trait Consumer<Item>: Send + Sized {
type Folder: Folder<Item, Result = Self::Result>;
type Reducer: Reducer<Self::Result> + Send;
type Result: Send;

fn split_off_left(&self) -> (Self, Self::Reducer);

fn into_folder(self) -> Self::Folder;

fn is_full(&self) -> bool {
false
}
}

pub trait IndexedConsumer<Item>: Consumer<Item> {
fn split_at(self, index: usize) -> (Self, Self, Self::Reducer);
}

pub trait Folder<Item>: Sized {
type Result;

fn is_full(&self) -> bool;

fn complete(self) -> Self::Result;

fn consume(self, item: Item) -> Self;

fn consume_iter<I>(mut self, iter: I) -> Self
where
I: IntoIterator<Item = Item>,
{
for item in iter {
self = self.consume(item);
if self.is_full() {
break;
}
}

self
}
}

pub trait Reducer<Result> {
fn reduce(self, left: Result, right: Result) -> Result;
}

pub trait Executor {
fn exec<P, C>(self, producer: P, consumer: C) -> C::Result
where
P: Producer,
C: Consumer<P::Item>;

fn exec_indexed<P, C>(self, producer: P, consumer: C) -> C::Result
where
P: IndexedProducer,
C: IndexedConsumer<P::Item>;
}

+ 0
- 7
parallel-iterator/src/no_op.rs View File

@@ -1,7 +0,0 @@
use super::Reducer;

pub struct NoOpReducer;

impl Reducer<()> for NoOpReducer {
fn reduce(self, _left: (), _right: ()) {}
}

Loading…
Cancel
Save