Module composable::derive_macros

source ·
Expand description

Derive macros used to ease the creation of recursive reducers.

  • RecursiveReducer
    #[derive(RecursiveReducer)] on a enum or struct that contains other Reducer types will derive a Reducer implementation for it.
  • TryInto
    #[derive(TryInto)] on a Action whose variants contain another Reducer’s Actions allows an attempted conversion to…
  • From
    #[derive(TryInto)] on a Action whose variants contain another Reducer’s Actions allows an attempted conversion from…

These macros produce efficient implementations of the Reducer, std::convert::TryInto and std::convert::From traits so that they do not have to be implemented manually.

§Automatic Derived Reducers

Two other types are valid Reducers whenever they contain a Reducer.

These do not require the RecursiveReducer and automatically apply.

§Composite Reducers

A RecursiveReducer struct represents a parent-child relationship between Reducers. This is the most common use of RecursiveReducer in large applications and forms the core “Composability” of the Composable Architecture.

The application is broken up into different modules representing the different Domains or Features of the application; each with its own Actions and State.

These Reducers are then collected into various composite Reducers that contain and coordinate between them.

Each composite Reducer is written with the knowledge of its own Actions and the Actions of its immediate children. The Actions of its parent are unknown to it and (by convention) it does not traffic in the Actions of its grandchildren.

Deciding which Domains need to be coordinated between, and thus should be siblings under a parent Domains, is the art of designing the application with an architecture like this one.

Even though the application struct recursively contains the States of all of its Features it usually does not end up being very “tall.”

See Unidirectional Event Architecture for more.

mod A {
    #[derive(Default)]
    pub struct State { /* … */ }

    #[derive(Clone)] // ⒈
    pub enum Action { /* … */ }

    impl Reducer for State {
        type Action = Action;
        type Output = Self;
         
        fn reduce(&mut self, action: Action, send: impl Effects<Action>) {
            match action { /* … */ }
        }
    }
}

mod B {
    #[derive(Default)]
    pub struct State;

    #[derive(Clone)] // ⒈
    pub enum Action { /* … */ }

    impl Reducer for State {
        type Action = Action;
        type Output = Self;

        fn reduce(&mut self, action: Action, send: impl Effects<Action>) {
            match action { /* … */ }
        }
    }
}

#[derive(Default, RecursiveReducer)] // ⒉
struct State {
    a: A::State,
    b: B::State,
}

#[derive(Clone, From, TryInto)] // ⒊
enum Action {
    SomeAction, // parent actions
    SomeOtherAction,

    A(A::Action), // ⒋
    B(B::Action),
}

impl RecursiveReducer for State { // ⒌
    type Action = Action;

    fn reduce(&mut self, action: Action, send: impl Effects<Action>) {
        match action {
            Action::SomeAction => { /* … */ }
            Action::SomeOtherAction => { /* … */ }

            // in this example, the parent reducer has
            // no explicit handling of any child actions
            _ => {}
        }
    }
}
  1. Now that Actions are being passed to multiple Reducers they must be Clone.

  2. The RecursiveReducer derive macro constructs a recursive Reducer from the struct.

  3. The From and TryInfo derive macros ensure that conversions work, when they should, between parent and child Actions. These conversions utilize #4…

  4. The parent has one (and only one) Action for the Actions of each of its children.

  5. Finally, an implementation of the RecursiveReducer trait containing the parent’s reduce method. RecursiveReducer::reduce is run before the Reducer::reduce methods of its fields. Resulting in:

    • self.reduce(), then
    • self.a.reduce(), then
    • self.b.reduce().

§Ignoring fields

Compound Reducers often contain fields other than the child Reducers. After all, it has its own Reducer and that Reducer may need its own state.

The RecursiveReducer macro comes with an associated attribute that allows it to skip struct members that should not ne made part of the Reducer recursion.

#[derive(RecursiveReducer)]
struct State {
    a: A::State,
    b: B::State,

    #[reducer(skip)]
    c: Vec<u32>,
}

§Alternate Reducers

A RecursiveReducer enum represents a single state that is best represented by an enumeration a separate reducers.

Alternate Reducers are less common than Composite Reducers so a more concrete example may help…

#[derive(RecursiveReducer)]
enum State {
    LoggedIn(authenticated::State),
    LoggedOut(unauthenticated::State),
}

#[derive(Clone, From, TryInto)]
enum Action {
    LoggedIn(authenticated::Action),
    LoggedOut(unauthenticated::Action),
}

impl RecursiveReducer for State {
    type Action = Action;

    fn reduce(&mut self, action: Action, send: impl Effects<Action>) {
        // logic independent of the user’s authentication
    }
}

authenticated::Actions will only run when the state is LoggedIn and vice-versa..



Now, the automatic derive reducer behavior of Option is easy to described. It behaves is as if it were:

#[derive(RecursiveReducer)]
enum Option<T: Reducer> {
    #[reducer(skip)]
    None,
    Some(T),
}

Although, currently, the RecursiveReducer macro does not work with generic parameters on the type it is attempting to derive the Reducer trait for.

Re-exports§

  • pub use derive_more::From;
  • pub use derive_more::TryInto;

Traits§

Derive Macros§