Skip to content

Commit

Permalink
Add a module for common system chain/pipe adapters (bevyengine#5776)
Browse files Browse the repository at this point in the history
# Objective

Right now, users have to implement basic system adapters such as `Option` <-> `Result` conversions by themselves. This is slightly annoying and discourages the use of system chaining.

## Solution

Add the module `system_adapter` to the prelude, which contains a collection of common adapters. This is very ergonomic in practice.

## Examples

Convenient early returning.

```rust
use bevy::prelude::*;

App::new()
    // If the system fails, just try again next frame.
    .add_system(pet_dog.chain(system_adapter::ignore))
    .run();

#[derive(Component)]
struct Dog;

fn pet_dog(dogs: Query<(&Name, Option<&Parent>), With<Dog>>) -> Option<()> {
    let (dog, dad) = dogs.iter().next()?;
    println!("You pet {dog}. He/she/they are a good boy/girl/pupper.");
    let (dad, _) = dogs.get(dad?.get()).ok()?;
    println!("Their dad's name is {dad}");
    Some(())
}
```

Converting the output of a system

```rust
use bevy::prelude::*;

App::new()
    .add_system(
        find_name
            .chain(system_adapter::new(String::from))
            .chain(spawn_with_name),
    )
    .run();

fn find_name() -> &'static str { /* ... */ }
fn spawn_with_name(In(name): In<String>, mut commands: Commands) {
    commands.spawn().insert(Name::new(name));
}
```
---

## Changelog

* Added the module `bevy_ecs::prelude::system_adapter`, which contains a collection of common system chaining adapters.
  * `new` - Converts a regular fn to a system adapter.
  * `unwrap` - Similar to `Result::unwrap`
  * `ignore` - Discards the output of the previous system.
  • Loading branch information
JoJoJet authored and ItsDoot committed Feb 1, 2023
1 parent d818a1f commit a410c55
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 3 deletions.
6 changes: 3 additions & 3 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ pub mod prelude {
StageLabel, State, SystemLabel, SystemSet, SystemStage,
},
system::{
Commands, In, IntoChainSystem, IntoExclusiveSystem, IntoSystem, Local, NonSend,
NonSendMut, ParallelCommands, ParamSet, Query, RemovedComponents, Res, ResMut,
Resource, System, SystemParamFunction,
adapter as system_adapter, Commands, In, IntoChainSystem, IntoExclusiveSystem,
IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, ParamSet, Query,
RemovedComponents, Res, ResMut, Resource, System, SystemParamFunction,
},
world::{FromWorld, Mut, World},
};
Expand Down
128 changes: 128 additions & 0 deletions crates/bevy_ecs/src/system/system_chaining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,131 @@ where
}
}
}

/// A collection of common adapters for [chaining](super::ChainSystem) the result of a system.
pub mod adapter {
use crate::system::In;
use std::fmt::Debug;

/// Converts a regular function into a system adapter.
///
/// # Examples
/// ```
/// use bevy_ecs::prelude::*;
///
/// return1
/// .chain(system_adapter::new(u32::try_from))
/// .chain(system_adapter::unwrap)
/// .chain(print);
///
/// fn return1() -> u64 { 1 }
/// fn print(In(x): In<impl std::fmt::Debug>) {
/// println!("{x:?}");
/// }
/// ```
pub fn new<T, U>(mut f: impl FnMut(T) -> U) -> impl FnMut(In<T>) -> U {
move |In(x)| f(x)
}

/// System adapter that unwraps the `Ok` variant of a [`Result`].
/// This is useful for fallible systems that should panic in the case of an error.
///
/// There is no equivalent adapter for [`Option`]. Instead, it's best to provide
/// an error message and convert to a `Result` using `ok_or{_else}`.
///
/// # Examples
///
/// Panicking on error
///
/// ```
/// use bevy_ecs::prelude::*;
/// #
/// # #[derive(StageLabel)]
/// # enum CoreStage { Update };
///
/// // Building a new schedule/app...
/// # use bevy_ecs::schedule::SystemStage;
/// # let mut sched = Schedule::default(); sched
/// # .add_stage(CoreStage::Update, SystemStage::single_threaded())
/// .add_system_to_stage(
/// CoreStage::Update,
/// // Panic if the load system returns an error.
/// load_save_system.chain(system_adapter::unwrap)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system which may fail irreparably.
/// fn load_save_system() -> Result<(), std::io::Error> {
/// let save_file = open_file("my_save.json")?;
/// dbg!(save_file);
/// Ok(())
/// }
/// # fn open_file(name: &str) -> Result<&'static str, std::io::Error>
/// # { Ok("hello world") }
/// ```
pub fn unwrap<T, E: Debug>(In(res): In<Result<T, E>>) -> T {
res.unwrap()
}

/// System adapter that ignores the output of the previous system in a chain.
/// This is useful for fallible systems that should simply return early in case of an `Err`/`None`.
///
/// # Examples
///
/// Returning early
///
/// ```
/// use bevy_ecs::prelude::*;
///
/// // Marker component for an enemy entity.
/// #[derive(Component)]
/// struct Monster;
/// #
/// # #[derive(StageLabel)]
/// # enum CoreStage { Update };
///
/// // Building a new schedule/app...
/// # use bevy_ecs::schedule::SystemStage;
/// # let mut sched = Schedule::default(); sched
/// # .add_stage(CoreStage::Update, SystemStage::single_threaded())
/// .add_system_to_stage(
/// CoreStage::Update,
/// // If the system fails, just move on and try again next frame.
/// fallible_system.chain(system_adapter::ignore)
/// )
/// // ...
/// # ;
/// # let mut world = World::new();
/// # sched.run(&mut world);
///
/// // A system which may return early. It's more convenient to use the `?` operator for this.
/// fn fallible_system(
/// q: Query<Entity, With<Monster>>
/// ) -> Option<()> {
/// let monster_id = q.iter().next()?;
/// println!("Monster entity is {monster_id:?}");
/// Some(())
/// }
/// ```
pub fn ignore<T>(In(_): In<T>) {}

#[cfg(test)]
#[test]
fn assert_systems() {
use std::str::FromStr;

use crate::{prelude::*, system::assert_is_system};

/// Mocks a system that returns a value of type `T`.
fn returning<T>() -> T {
unimplemented!()
}

assert_is_system(returning::<Result<u32, std::io::Error>>.chain(unwrap));
assert_is_system(returning::<Option<()>>.chain(ignore));
assert_is_system(returning::<&str>.chain(new(u64::from_str)).chain(unwrap));
}
}

0 comments on commit a410c55

Please sign in to comment.