Try   HackMD

System config genericization

Genericizing the current design

Basically if we look at NodeConfig as it is today:

pub struct NodeConfig<T> {
    pub(crate) node: T,
    pub(crate) graph_info: GraphInfo,
    pub(crate) conditions: Vec<BoxedCondition>,
}

We want to not allow/include GraphInfo for observer configs, and just want to support run conditions. Maybe we'll support observer ordering in the future but for now it is controversial, and its not hard to change it to support it in the future if we follow the design below.

So, we have a T there, which means we know what kind of node we're handling. We can bound T with a new trait that lets us also specify the kind of metadata to store:

pub trait NodeType {
    type Metadata; // <-- Used in NodeConfig
    type GroupMetadata; // <-- Used in NodeConfigs (plural)
}

pub struct NodeConfig<T: NodeType> {
    pub(crate) node: T,
    pub(crate) metadata: T::Metadata,
    pub(crate) conditions: Vec<BoxedCondition>,
}

And to maintain compatibility, we implement NodeType for our two pre-existing node types:

impl NodeType for ScheduleSystem {
    type Metadata = GraphInfo;
    type GroupMetadata = Chain;
}

impl NodeType for InternedSystemSet {
    type Metadata = GraphInfo;
    type GroupMetadata = Chain;
}

So they still get to have NodeConfig hold their GraphInfo stuff. NodeConfigs (plural) is a simple change as well:

pub enum NodeConfigs<T: NodeType> { // <-- add the bound here
    NodeConfig(NodeConfig<T>),
    Configs {
        configs: Vec<NodeConfigs<T>>,
        collective_conditions: Vec<BoxedCondition>,
        metadata: T::GroupMetadata, // <-- use the associated type here
    },
}

Now anytime we have functions that need to operate on the GraphInfo, like adding system sets, ordering, etc. we can just use a bound T: NodeType<Metadata = GraphInfo>.

The IntoSystemConfigs and IntoSystemSetConfigs traits are basically the same, except they operate on different node types. Since we have a NodeType trait now, we can combine them into a single trait with a T: NodeType bound:

pub trait IntoNodeConfigs<T: NodeType, Marker>: Sized {
    fn into_configs(self) -> NodeConfigs<T>;
    
    // For the helper functions like in_set, before, after, etc.
    // we just add a bound `T: NodeType<Metadata = GraphInfo>` to
    // the function.
    
    fn in_set(self, set: impl SystemSet) -> NodeConfigs<T>
    where
        T: NodeType<Metadata = NodeInfo>
    {
        // ...
    }
    
    // Except distributive_run_if and run_if, since conditions are always
    // around regardless of node type. So they can be left unchanged.
    
    fn distributive_run_if<M>(self, condition: impl Condition<M> + Clone) -> NodeConfigs<T> {
        // ...
    }
    
    fn run_if<M>(self, condition: impl Condition<M>) -> NodeConfigs<T> {
        // ...
    }
    
    // For chain and chain_ignore_deferred, we instead bound with
    // `T: NodeType<GroupMetadata = Chain>`
    
    fn chain(self) -> NodeConfigs<T>
    where
        T: NodeType<GroupMetadata = Chain>
    {
        // ...
    }
    
    fn chain_ignore_deferred(self) -> NodeConfigs<T>
    where
        T: NodeType<GroupMetadata = Chain>
    {
        // ...
    }
}

Now for add_systems and configure_sets, they use the IntoNodeConfigs trait but specify their node type:

impl App {
    pub fn add_systems<M>(
        &mut self,
        schedule: impl ScheduleLabel,
        systems: impl IntoNodeConfigs<ScheduleSystem, M>,
    ) -> &mut Self;
    
    pub fn configure_sets<M>( // <-- configure sets does get a marker generic now, NBD though
        &mut self,
        schedule: impl ScheduleLabel,
        sets: impl IntoNodeConfigs<InternedSystemSet, M>,
    ) -> &mut Self;
}

Supporting observer configs

Now with all that done, adding observer configuration support is incredibly easy. First add a new node type:

pub type ObserveSystem<E, B> = Box<dyn ObserverSystem<E, B>>;

impl<E: 'static, B: Bundle> Nodetype for ObserveSystem<E, B> {
    type Metadata = (); // <------- No metadata for now, maybe we add some later.
    type GroupMetadata = (); // <--/
}

And also implement IntoNodeConfigs for observer systems. We can mostly copy the impl for IntoNodeConfigs<ScheduleSystem> and change a few things:

impl<E, B, S, Marker> IntoNodeConfigs<ObserveSystem<E, B>, (Infallible, Marker)> for S
where
    E: 'static,
    B: Bundle,
    S: IntoObserverSystem<E, B, Marker, ()>,
{
    fn into_configs(self) -> NodeConfigs<ObserveSystem<E, B>> {
        // ...
    }
}

impl<E, B, S, Marker> IntoNodeConfigs<ObserveSystem<E, B>, (Fallible, Marker)> for S
where
    E: 'static,
    B: Bundle,
    S: IntoObserverSystem<E, B, Marker, Result>,
{
    fn into_configs(self) -> NodeConfigs<ObserveSystem<E, B>> {
        // ...
    }
}

Then, change the function signature of add_observer to accept configs instead. Maybe we should also pluralize the function name?

impl App {
    pub fn add_observers<E: Event, B: Bundle, M>(
        &mut self,
        observers: impl IntoNodeConfigs<ObserveSystem<E, B>, M>,
    ) -> &mut Self;
}

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
This design leaves out something that needs to be handled however!

Collective conditions can't be easily cloned and pushed alongside every observer system, because conditons aren't required to be Cloneable! So I'll leave someone else to figure that out. It should be possible to just push them into a hash map and reference them in each observer container, probably.