diff --git a/.travis.yml b/.travis.yml index 284525d..49a6182 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,3 +54,24 @@ jobs: - cargo +nightly tarpaulin -v --all-features --out Xml after_success: - bash <(curl -s https://codecov.io/bash) + + - name: experimental + stage: test + rust: nightly + os: linux + script: + - cargo +nightly test -v --all-features + + - name: experimental + stage: test + rust: nightly + os: osx + script: + - cargo +nightly test -v --all-features + + - name: experimental + stage: test + rust: nightly + os: windows + script: + - cargo +nightly test -v --all-features diff --git a/Cargo.toml b/Cargo.toml index 92d9b40..f9a5ff9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,15 @@ readme = "README.md" keywords = ["redux", "flux", "reactive", "state"] categories = ["asynchronous", "gui"] +[features] +parallel = ["rayon"] + [badges] travis-ci = { repository = "brunocodutra/reducer" } codecov = { repository = "brunocodutra/reducer" } [dependencies] +rayon = { version = "1.0.3", optional = true } [dev-dependencies] iui = "0.3.0" diff --git a/README.md b/README.md index a83643c..042a485 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ use reducer::*; The full API documentation is available on [docs.rs] +# Experimental Features + +The following cargo feature flags are available (refer to the [documentation][docs.rs] for details): +* `parallel` (depends on nightly Rust) + + Relies on specialization ([RFC 1210]) to provide automatic fork-join parallelism using [Rayon]. ## Examples @@ -63,6 +69,9 @@ Reducer is distributed under the terms of the MIT license, see [LICENSE] for det [crates.io]: https://crates.io/crates/reducer [docs.rs]: https://docs.rs/reducer +[RFC 1210]: https://github.com/rust-lang/rust/issues/31844 +[Rayon]: https://crates.io/crates/rayon + [issues]: https://github.com/brunocodutra/reducer/issues [pulls]: https://github.com/brunocodutra/reducer/pulls [examples]: https://github.com/brunocodutra/reducer/tree/master/examples diff --git a/src/lib.rs b/src/lib.rs index 173cac3..3c5bff9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,8 +81,25 @@ //! store.dispatch(Div(7)).unwrap(); // displays "2" //! } //! ``` +//! # Experimental Features +//! +//! The following cargo feature flags are available: +//! * `parallel` (depends on nightly Rust) +//! +//! This feature flag takes advantage of the experimental support for specialization available +//! on nightly Rust ([RFC 1210](https://github.com/rust-lang/rust/issues/31844)), to +//! automatically parallelize tuples of +//! [Sync](https://doc.rust-lang.org/nightly/std/marker/trait.Sync.html) / +//! [Send](https://doc.rust-lang.org/nightly/std/marker/trait.Send.html) Reducers +//! using [Rayon](https://crates.io/crates/rayon). + +#![cfg_attr(feature = "parallel", feature(specialization))] + +#[cfg(feature = "parallel")] +extern crate rayon; mod macros; +mod mock; mod reactor; mod reducer; diff --git a/src/macros.rs b/src/macros.rs index 27c92ca..16ca73a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -16,3 +16,50 @@ macro_rules! dedupe_docs { $( $definition )+ }; } + +macro_rules! specialize { + ( #[cfg($conditional:meta)] default $( $definition:tt )+ ) => { + #[cfg($conditional)] + default $( $definition )+ + + #[cfg(not($conditional))] + $( $definition )+ + } +} + +#[cfg(feature = "parallel")] +macro_rules! join { + ( ) => { + () + }; + + ( $a:ident $(,)* ) => { + ($a(),) + }; + + ( $a:ident, $b:ident $(,)* ) => { + rayon::join($a, $b) + }; + + ( $a:ident, $b:ident $(, $tail:ident )+ $(,)* ) => { + { + let ($a, ($b $(, $tail )+)) = rayon::join($a, || join!($b $(, $tail )+)); + ($a, $b $(, $tail )+) + } + }; +} + +#[cfg(test)] +mod test { + #[cfg(feature = "parallel")] + #[test] + fn join() { + let a = || 5; + let b = || 1; + let c = || 3; + + assert_eq!(join!(a), (5,)); + assert_eq!(join!(a, b), (5, 1)); + assert_eq!(join!(a, b, c), (5, 1, 3)); + } +} diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..c56869b --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,106 @@ +#![cfg(test)] + +use reactor::Reactor; +use reducer::Reducer; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::rc::Rc; + +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct NotSync(RefCell); + +impl NotSync { + pub fn new(t: T) -> Self { + NotSync(RefCell::new(t)) + } +} + +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct NotSyncOrSend(Rc); + +impl NotSyncOrSend { + pub fn new(t: T) -> Self { + NotSyncOrSend(Rc::new(t)) + } +} + +#[derive(Debug, Default, Clone, Eq, PartialEq)] +pub struct MockReducer { + actions: Vec, +} + +impl MockReducer { + pub fn new(actions: Vec) -> Self { + Self { actions } + } +} + +impl Reducer for MockReducer { + fn reduce(&mut self, action: A) { + self.actions.push(action); + } +} + +impl Reducer> for MockReducer { + fn reduce(&mut self, action: NotSync) { + self.actions.push((*action.0.borrow()).clone()); + } +} + +impl Reducer> for MockReducer { + fn reduce(&mut self, action: NotSyncOrSend) { + self.actions.push((*action.0).clone()); + } +} + +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] +pub struct MockReactor(PhantomData); + +impl Reactor for MockReactor { + type Output = S; + + fn react(&self, state: &S) -> Self::Output { + state.clone() + } +} + +impl Reactor> for MockReactor { + type Output = S; + + fn react(&self, action: &NotSync) -> Self::Output { + (*action.0.borrow()).clone() + } +} + +impl Reactor> for MockReactor { + type Output = S; + + fn react(&self, action: &NotSyncOrSend) -> Self::Output { + (*action.0).clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn react() { + let reactor = MockReactor::default(); + + assert_eq!(reactor.react(&5), 5); + assert_eq!(reactor.react(&NotSync::new(1)), 1); + assert_eq!(reactor.react(&NotSyncOrSend::new(3)), 3); + } + + #[test] + fn reduce() { + let mut state = MockReducer::default(); + + state.reduce(5); + state.reduce(NotSync::new(1)); + state.reduce(NotSyncOrSend::new(3)); + + assert_eq!(state, MockReducer::new(vec![5, 1, 3])); + } +} diff --git a/src/reactor/array.rs b/src/reactor/array.rs index e28accb..9d7f3c2 100644 --- a/src/reactor/array.rs +++ b/src/reactor/array.rs @@ -33,6 +33,7 @@ impl_reactor_for_array!( #[cfg(test)] mod tests { use super::*; + use mock::*; macro_rules! test_reactor_for_array { () => {}; @@ -40,11 +41,11 @@ mod tests { ( $head:ident $(, $tail:ident )* $(,)* ) => { #[test] fn $head() { - let reactors = [MockReactor; count!($( $tail, )*)]; + let reactors = [MockReactor::default(); count!($( $tail, )*)]; assert_eq!(reactors.react(&5), [5; count!($( $tail, )*)]); - assert_eq!(reactors.react(&1), [1; count!($( $tail, )*)]); - assert_eq!(reactors.react(&3), [3; count!($( $tail, )*)]); + assert_eq!(reactors.react(&NotSync::new(1)), [1; count!($( $tail, )*)]); + assert_eq!(reactors.react(&NotSyncOrSend::new(3)), [3; count!($( $tail, )*)]); } test_reactor_for_array!($( $tail, )*); diff --git a/src/reactor/mock.rs b/src/reactor/mock.rs deleted file mode 100644 index 4b9b61e..0000000 --- a/src/reactor/mock.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![cfg(test)] - -use reactor::Reactor; - -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct MockReactor; - -impl Reactor for MockReactor { - type Output = S; - - fn react(&self, state: &S) -> Self::Output { - state.clone() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn react() { - let reactor = MockReactor; - - assert_eq!(reactor.react(&5), 5); - assert_eq!(reactor.react(&1), 1); - assert_eq!(reactor.react(&3), 3); - } -} diff --git a/src/reactor/mod.rs b/src/reactor/mod.rs index 782b219..8492048 100644 --- a/src/reactor/mod.rs +++ b/src/reactor/mod.rs @@ -1,5 +1,4 @@ mod array; -mod mock; mod option; mod reference; mod sender; @@ -79,16 +78,14 @@ pub trait Reactor { fn react(&self, state: &S) -> Self::Output; } -#[cfg(test)] -pub use self::mock::*; - #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn react() { - let reactor: &Reactor<_, Output = _> = &MockReactor; + let reactor: &Reactor<_, Output = _> = &MockReactor::default(); assert_eq!(reactor.react(&5), 5); assert_eq!(reactor.react(&1), 1); diff --git a/src/reactor/option.rs b/src/reactor/option.rs index 0017688..ae13994 100644 --- a/src/reactor/option.rs +++ b/src/reactor/option.rs @@ -18,22 +18,23 @@ where #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn some() { - let reactor = Some(MockReactor); + let reactor = Some(MockReactor::default()); assert_eq!(reactor.react(&5), Some(5)); - assert_eq!(reactor.react(&1), Some(1)); - assert_eq!(reactor.react(&3), Some(3)); + assert_eq!(reactor.react(&NotSync::new(1)), Some(1)); + assert_eq!(reactor.react(&NotSyncOrSend::new(3)), Some(3)); } #[test] fn none() { - let reactor: Option = None; + let reactor: Option> = None; assert_eq!(reactor.react(&5), None); - assert_eq!(reactor.react(&1), None); - assert_eq!(reactor.react(&3), None); + assert_eq!(reactor.react(&NotSync::new(1)), None); + assert_eq!(reactor.react(&NotSyncOrSend::new(3)), None); } } diff --git a/src/reactor/reference.rs b/src/reactor/reference.rs index f355421..b6f9d02 100644 --- a/src/reactor/reference.rs +++ b/src/reactor/reference.rs @@ -14,14 +14,15 @@ where #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn react() { - let reactor = &MockReactor; + let reactor = &MockReactor::default(); let reactor = &reactor; assert_eq!(reactor.react(&5), 5); - assert_eq!(reactor.react(&1), 1); - assert_eq!(reactor.react(&3), 3); + assert_eq!(reactor.react(&NotSync::new(1)), 1); + assert_eq!(reactor.react(&NotSyncOrSend::new(3)), 3); } } diff --git a/src/reactor/slice.rs b/src/reactor/slice.rs index f0f84bd..593f3e2 100644 --- a/src/reactor/slice.rs +++ b/src/reactor/slice.rs @@ -18,13 +18,14 @@ where #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn react() { - let reactor: &[MockReactor] = &[MockReactor, MockReactor, MockReactor]; + let reactor: &[MockReactor<_>] = &[MockReactor::default(); 42]; - assert_eq!(reactor.react(&5), vec![5, 5, 5].into_boxed_slice()); - assert_eq!(reactor.react(&1), vec![1, 1, 1].into_boxed_slice()); - assert_eq!(reactor.react(&3), vec![3, 3, 3].into_boxed_slice()); + assert_eq!(reactor.react(&5), vec![5; 42].into()); + assert_eq!(reactor.react(&NotSync::new(1)), vec![1; 42].into()); + assert_eq!(reactor.react(&NotSyncOrSend::new(3)), vec![3; 42].into()); } } diff --git a/src/reactor/tuple.rs b/src/reactor/tuple.rs index be030fc..c002c0c 100644 --- a/src/reactor/tuple.rs +++ b/src/reactor/tuple.rs @@ -31,6 +31,7 @@ impl_reactor_for_tuples!(_12, _11, _10, _09, _08, _07, _06, _05, _04, _03, _02, #[cfg(test)] mod tests { use super::*; + use mock::*; macro_rules! test_reactor_for_tuples { () => {}; @@ -47,11 +48,14 @@ mod tests { } } - impl Reactor for $head { + impl Reactor for $head + where + MockReactor: Reactor, + { type Output = Self; fn react(&self, state: &S) -> Self::Output { - $head::new(state.clone()) + $head::new(MockReactor::default().react(state)) } } @@ -60,8 +64,8 @@ mod tests { let reactor = ($head::default(), $( $tail::default(), )*); assert_eq!(reactor.react(&5), ($head::new(5), $( $tail::new(5), )*)); - assert_eq!(reactor.react(&1), ($head::new(1), $( $tail::new(1), )*)); - assert_eq!(reactor.react(&3), ($head::new(3), $( $tail::new(3), )*)); + assert_eq!(reactor.react(&NotSync::new(1)), ($head::new(1), $( $tail::new(1), )*)); + assert_eq!(reactor.react(&NotSyncOrSend::new(3)), ($head::new(3), $( $tail::new(3), )*)); } test_reactor_for_tuples!($( $tail, )*); diff --git a/src/reducer/arc.rs b/src/reducer/arc.rs index e81cec6..c8f93e5 100644 --- a/src/reducer/arc.rs +++ b/src/reducer/arc.rs @@ -17,14 +17,15 @@ where #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn reduce() { let mut state = Arc::new(MockReducer::default()); state.reduce(5); - state.reduce(1); - state.reduce(3); + state.reduce(NotSync::new(1)); + state.reduce(NotSyncOrSend::new(3)); assert_eq!(state, Arc::new(MockReducer::new(vec![5, 1, 3]))); } diff --git a/src/reducer/mock.rs b/src/reducer/mock.rs deleted file mode 100644 index 6143a10..0000000 --- a/src/reducer/mock.rs +++ /dev/null @@ -1,36 +0,0 @@ -#![cfg(test)] - -use reducer::Reducer; - -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct MockReducer { - actions: Vec, -} - -impl MockReducer { - pub fn new(actions: Vec) -> Self { - Self { actions } - } -} - -impl Reducer for MockReducer { - fn reduce(&mut self, action: A) { - self.actions.push(action); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reduce() { - let mut state = MockReducer::default(); - - state.reduce(5); - state.reduce(1); - state.reduce(3); - - assert_eq!(state, MockReducer::new(vec![5, 1, 3])); - } -} diff --git a/src/reducer/mod.rs b/src/reducer/mod.rs index 5d6ee32..9ab4898 100644 --- a/src/reducer/mod.rs +++ b/src/reducer/mod.rs @@ -1,5 +1,4 @@ mod arc; -mod mock; mod rc; mod tuple; @@ -101,12 +100,10 @@ pub trait Reducer: 'static { fn reduce(&mut self, action: A); } -#[cfg(test)] -pub use self::mock::*; - #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn reduce() { diff --git a/src/reducer/rc.rs b/src/reducer/rc.rs index a6250c6..89f1ad8 100644 --- a/src/reducer/rc.rs +++ b/src/reducer/rc.rs @@ -14,14 +14,15 @@ where #[cfg(test)] mod tests { use super::*; + use mock::*; #[test] fn reduce() { let mut state = Rc::new(MockReducer::default()); state.reduce(5); - state.reduce(1); - state.reduce(3); + state.reduce(NotSync::new(1)); + state.reduce(NotSyncOrSend::new(3)); assert_eq!(state, Rc::new(MockReducer::new(vec![5, 1, 3]))); } diff --git a/src/reducer/tuple.rs b/src/reducer/tuple.rs index f8f53f2..6c003f1 100644 --- a/src/reducer/tuple.rs +++ b/src/reducer/tuple.rs @@ -13,11 +13,45 @@ macro_rules! impl_reducer_for_tuples { A: Clone, $head: Reducer, $( $tail: Reducer, )* + { + specialize!( + #[cfg(feature = "parallel")] + default fn reduce(&mut self, action: A) { + let ($head, $( $tail, )*) = self; + $head.reduce(action.clone()); + $( $tail.reduce(action.clone()); )* + } + ); + } + ); + + dedupe_docs!(($( $tail, )*), + /// Updates all reducers in the tuple in parallel. + /// + /// Currently implemented for tuples of up to 12 elements. + #[cfg(feature = "parallel")] + impl Reducer for ($head, $( $tail, )*) + where + A: Clone + Send, + $head: Reducer + Send, + $( $tail: Reducer + Send, )* { fn reduce(&mut self, action: A) { let ($head, $( $tail, )*) = self; - $head.reduce(action.clone()); - $( $tail.reduce(action.clone()); )* + + let $head = { + let action = action.clone(); + move || $head.reduce(action) + }; + + $( + let $tail = { + let action = action.clone(); + move || $tail.reduce(action) + }; + )* + + join!($head $(, $tail )*); } } ); @@ -31,6 +65,7 @@ impl_reducer_for_tuples!(_12, _11, _10, _09, _08, _07, _06, _05, _04, _03, _02, #[cfg(test)] mod tests { use super::*; + use mock::*; macro_rules! test_reducer_for_tuples { () => {}; @@ -41,7 +76,10 @@ mod tests { inner: MockReducer, } - impl Reducer for $head { + impl Reducer for $head + where + MockReducer: Reducer, + { fn reduce(&mut self, action: A) { self.inner.reduce(action); } @@ -52,8 +90,8 @@ mod tests { let mut states = ($head::default(), $( $tail::default(), )*); states.reduce(5); - states.reduce(1); - states.reduce(3); + states.reduce(NotSync::new(1)); + states.reduce(NotSyncOrSend::new(3)); let ($head, $( $tail, )*) = states; diff --git a/src/store.rs b/src/store.rs index 853125d..24e901e 100644 --- a/src/store.rs +++ b/src/store.rs @@ -44,12 +44,11 @@ impl> Store { #[cfg(test)] mod tests { use super::*; - use reactor::MockReactor; - use reducer::MockReducer; + use mock::*; #[test] fn default() { - let store = Store::, MockReactor>::default(); + let store = Store::, MockReactor<_>>::default(); assert_eq!(store.state, MockReducer::default()); assert_eq!(store.reactor, MockReactor::default()); @@ -58,7 +57,7 @@ mod tests { #[test] fn new() { let state = MockReducer::new(vec![42]); - let reactor = MockReactor; + let reactor = MockReactor::default(); let store = Store::new(state.clone(), &reactor); assert_eq!(store.state, state); @@ -67,29 +66,39 @@ mod tests { #[test] fn clone() { - let store = Store::new(MockReducer::<()>::default(), MockReactor); + let store = Store::new(MockReducer::<()>::default(), MockReactor::default()); assert_eq!(store, store.clone()); } #[test] fn dispatch() { - let mut store = Store::, MockReactor>::default(); + let mut store = Store::, MockReactor<_>>::default(); - assert_eq!(store.dispatch(5), MockReducer::new(vec![5])); - assert_eq!(store.dispatch(1), MockReducer::new(vec![5, 1])); - assert_eq!(store.dispatch(3), MockReducer::new(vec![5, 1, 3])); + let a = NotSync::new(5); + assert_eq!(store.dispatch(a), MockReducer::new(vec![5])); + + let a = NotSync::new(1); + assert_eq!(store.dispatch(a), MockReducer::new(vec![5, 1])); + + let a = NotSyncOrSend::new(3); + assert_eq!(store.dispatch(a), MockReducer::new(vec![5, 1, 3])); } #[test] fn subscribe() { - let mut store: Store<_, Option> = Store::new(MockReducer::default(), None); + let mut store: Store<_, Option>> = Store::new(MockReducer::default(), None); assert_eq!(store.dispatch(0), None); - store.subscribe(Some(MockReactor)); + store.subscribe(Some(MockReactor::default())); + + let a = NotSync::new(5); + assert_eq!(store.dispatch(a), Some(MockReducer::new(vec![0, 5]))); + + let a = NotSync::new(1); + assert_eq!(store.dispatch(a), Some(MockReducer::new(vec![0, 5, 1]))); - assert_eq!(store.dispatch(5), Some(MockReducer::new(vec![0, 5]))); - assert_eq!(store.dispatch(1), Some(MockReducer::new(vec![0, 5, 1]))); - assert_eq!(store.dispatch(3), Some(MockReducer::new(vec![0, 5, 1, 3]))); + let a = NotSyncOrSend::new(3); + assert_eq!(store.dispatch(a), Some(MockReducer::new(vec![0, 5, 1, 3]))); } }