bevy_derive: Add derives for Deref and DerefMut (#4328)

# Objective

A common pattern in Rust is the [newtype](https://doc.rust-lang.org/rust-by-example/generics/new_types.html). This is an especially useful pattern in Bevy as it allows us to give common/foreign types different semantics (such as allowing it to implement `Component` or `FromWorld`) or to simply treat them as a "new type" (clever). For example, it allows us to wrap a common `Vec<String>` and do things like:

```rust
#[derive(Component)]
struct Items(Vec<String>);

fn give_sword(query: Query<&mut Items>) { 
  query.single_mut().0.push(String::from("Flaming Poisoning Raging Sword of Doom"));
}
```

> We could then define another struct that wraps `Vec<String>` without anything clashing in the query.

However, one of the worst parts of this pattern is the ugly `.0` we have to write in order to access the type we actually care about. This is why people often implement `Deref` and `DerefMut` in order to get around this.

Since it's such a common pattern, especially for Bevy, it makes sense to add a derive macro to automatically add those implementations.


## Solution

Added a derive macro for `Deref` and another for `DerefMut` (both exported into the prelude). This works on all structs (including tuple structs) as long as they only contain a single field:

```rust
#[derive(Deref)]
struct Foo(String);

#[derive(Deref, DerefMut)]
struct Bar {
  name: String,
}
```

This allows us to then remove that pesky `.0`:

```rust
#[derive(Component, Deref, DerefMut)]
struct Items(Vec<String>);

fn give_sword(query: Query<&mut Items>) { 
  query.single_mut().push(String::from("Flaming Poisoning Raging Sword of Doom"));
}
```

### Alternatives

There are other alternatives to this such as by using the [`derive_more`](https://crates.io/crates/derive_more) crate. However, it doesn't seem like we need an entire crate just yet since we only need `Deref` and `DerefMut` (for now).

### Considerations

One thing to consider is that the Rust std library recommends _not_ using `Deref` and `DerefMut` for things like this: "`Deref` should only be implemented for smart pointers to avoid confusion" ([reference](https://doc.rust-lang.org/std/ops/trait.Deref.html)). Personally, I believe it makes sense to use it in the way described above, but others may disagree.

### Additional Context

Discord: https://discord.com/channels/691052431525675048/692572690833473578/956648422163746827 (controversiality discussed [here](https://discord.com/channels/691052431525675048/692572690833473578/956711911481835630))

---

## Changelog

- Add `Deref` derive macro (exported to prelude)
- Add `DerefMut` derive macro (exported to prelude)
- Updated most newtypes in examples to use one or both derives

Co-authored-by: MrGVSV <49806985+MrGVSV@users.noreply.github.com>
This commit is contained in:
MrGVSV 2022-03-29 02:10:06 +00:00
parent 28ba87e6c8
commit f16768d868
18 changed files with 174 additions and 37 deletions

View File

@ -0,0 +1,69 @@
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Index, Member, Type};
pub fn derive_deref(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ident = &ast.ident;
let (field_member, field_type) = match get_inner_field(&ast, false) {
Ok(items) => items,
Err(err) => {
return err.into_compile_error().into();
}
};
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics ::std::ops::Deref for #ident #ty_generics #where_clause {
type Target = #field_type;
fn deref(&self) -> &Self::Target {
&self.#field_member
}
}
})
}
pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ident = &ast.ident;
let (field_member, _) = match get_inner_field(&ast, true) {
Ok(items) => items,
Err(err) => {
return err.into_compile_error().into();
}
};
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics ::std::ops::DerefMut for #ident #ty_generics #where_clause {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.#field_member
}
}
})
}
fn get_inner_field(ast: &DeriveInput, is_mut: bool) -> syn::Result<(Member, &Type)> {
match &ast.data {
Data::Struct(data_struct) if data_struct.fields.len() == 1 => {
let field = data_struct.fields.iter().next().unwrap();
let member = field
.ident
.as_ref()
.map(|name| Member::Named(name.clone()))
.unwrap_or_else(|| Member::Unnamed(Index::from(0)));
Ok((member, &field.ty))
}
_ => {
let msg = if is_mut {
"DerefMut can only be derived for structs with a single field"
} else {
"Deref can only be derived for structs with a single field"
};
Err(syn::Error::new(Span::call_site().into(), msg))
}
}
}

View File

@ -2,6 +2,7 @@ extern crate proc_macro;
mod app_plugin; mod app_plugin;
mod bevy_main; mod bevy_main;
mod derefs;
mod enum_variant_meta; mod enum_variant_meta;
mod modules; mod modules;
@ -15,6 +16,61 @@ pub fn derive_dynamic_plugin(input: TokenStream) -> TokenStream {
app_plugin::derive_dynamic_plugin(input) app_plugin::derive_dynamic_plugin(input)
} }
/// Implements [`Deref`] for _single-item_ structs. This is especially useful when
/// utilizing the [newtype] pattern.
///
/// If you need [`DerefMut`] as well, consider using the other [derive] macro alongside
/// this one.
///
/// # Example
///
/// ```
/// use bevy_derive::Deref;
///
/// #[derive(Deref)]
/// struct MyNewtype(String);
///
/// let foo = MyNewtype(String::from("Hello"));
/// assert_eq!(5, foo.len());
/// ```
///
/// [`Deref`]: std::ops::Deref
/// [newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
/// [`DerefMut`]: std::ops::DerefMut
/// [derive]: crate::derive_deref_mut
#[proc_macro_derive(Deref)]
pub fn derive_deref(input: TokenStream) -> TokenStream {
derefs::derive_deref(input)
}
/// Implements [`DerefMut`] for _single-item_ structs. This is especially useful when
/// utilizing the [newtype] pattern.
///
/// [`DerefMut`] requires a [`Deref`] implementation. You can implement it manually or use
/// Bevy's [derive] macro for convenience.
///
/// # Example
///
/// ```
/// use bevy_derive::{Deref, DerefMut};
///
/// #[derive(Deref, DerefMut)]
/// struct MyNewtype(String);
///
/// let mut foo = MyNewtype(String::from("Hello"));
/// foo.push_str(" World!");
/// assert_eq!("Hello World!", *foo);
/// ```
///
/// [`DerefMut`]: std::ops::DerefMut
/// [newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
/// [`Deref`]: std::ops::Deref
/// [derive]: crate::derive_deref
#[proc_macro_derive(DerefMut)]
pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
derefs::derive_deref_mut(input)
}
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn bevy_main(attr: TokenStream, item: TokenStream) -> TokenStream { pub fn bevy_main(attr: TokenStream, item: TokenStream) -> TokenStream {
bevy_main::bevy_main(attr, item) bevy_main::bevy_main(attr, item)

View File

@ -5,7 +5,7 @@ pub use crate::{
transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins, MinimalPlugins, transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins, MinimalPlugins,
}; };
pub use bevy_derive::bevy_main; pub use bevy_derive::{bevy_main, Deref, DerefMut};
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "bevy_audio")] #[cfg(feature = "bevy_audio")]

View File

@ -27,6 +27,7 @@ struct ContributorSelection {
idx: usize, idx: usize,
} }
#[derive(Deref, DerefMut)]
struct SelectTimer(Timer); struct SelectTimer(Timer);
#[derive(Component)] #[derive(Component)]
@ -161,7 +162,7 @@ fn select_system(
mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>, mut query: Query<(&Contributor, &mut Sprite, &mut Transform)>,
time: Res<Time>, time: Res<Time>,
) { ) {
if !timer.0.tick(time.delta()).just_finished() { if !timer.tick(time.delta()).just_finished() {
return; return;
} }

View File

@ -74,6 +74,7 @@ fn move_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Cam
* Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_seconds()); * Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_seconds());
} }
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer); struct PrintingTimer(Timer);
impl Default for PrintingTimer { impl Default for PrintingTimer {
@ -84,9 +85,9 @@ impl Default for PrintingTimer {
// System for printing the number of sprites on every tick of the timer // System for printing the number of sprites on every tick of the timer
fn print_sprite_count(time: Res<Time>, mut timer: Local<PrintingTimer>, sprites: Query<&Sprite>) { fn print_sprite_count(time: Res<Time>, mut timer: Local<PrintingTimer>, sprites: Query<&Sprite>) {
timer.0.tick(time.delta()); timer.tick(time.delta());
if timer.0.just_finished() { if timer.just_finished() {
info!("Sprites: {}", sprites.iter().count(),); info!("Sprites: {}", sprites.iter().count(),);
} }
} }

View File

@ -8,7 +8,7 @@ fn main() {
.run(); .run();
} }
#[derive(Component)] #[derive(Component, Deref, DerefMut)]
struct AnimationTimer(Timer); struct AnimationTimer(Timer);
fn animate_sprite( fn animate_sprite(
@ -21,8 +21,8 @@ fn animate_sprite(
)>, )>,
) { ) {
for (mut timer, mut sprite, texture_atlas_handle) in query.iter_mut() { for (mut timer, mut sprite, texture_atlas_handle) in query.iter_mut() {
timer.0.tick(time.delta()); timer.tick(time.delta());
if timer.0.just_finished() { if timer.just_finished() {
let texture_atlas = texture_atlases.get(texture_atlas_handle).unwrap(); let texture_atlas = texture_atlases.get(texture_atlas_handle).unwrap();
sprite.index = (sprite.index + 1) % texture_atlas.textures.len(); sprite.index = (sprite.index + 1) % texture_atlas.textures.len();
} }

View File

@ -145,9 +145,9 @@ fn print_mesh_count(
mut timer: Local<PrintingTimer>, mut timer: Local<PrintingTimer>,
sprites: Query<(&Handle<Mesh>, &ComputedVisibility)>, sprites: Query<(&Handle<Mesh>, &ComputedVisibility)>,
) { ) {
timer.0.tick(time.delta()); timer.tick(time.delta());
if timer.0.just_finished() { if timer.just_finished() {
info!( info!(
"Meshes: {} - Visible Meshes {}", "Meshes: {} - Visible Meshes {}",
sprites.iter().len(), sprites.iter().len(),
@ -156,6 +156,7 @@ fn print_mesh_count(
} }
} }
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer); struct PrintingTimer(Timer);
impl Default for PrintingTimer { impl Default for PrintingTimer {

View File

@ -22,7 +22,9 @@ fn main() {
// Number of cubes to spawn across the x, y, and z axis // Number of cubes to spawn across the x, y, and z axis
const NUM_CUBES: u32 = 6; const NUM_CUBES: u32 = 6;
#[derive(Deref)]
struct BoxMeshHandle(Handle<Mesh>); struct BoxMeshHandle(Handle<Mesh>);
#[derive(Deref)]
struct BoxMaterialHandle(Handle<StandardMaterial>); struct BoxMaterialHandle(Handle<StandardMaterial>);
/// Startup system which runs only once and generates our Box Mesh /// Startup system which runs only once and generates our Box Mesh
@ -84,8 +86,8 @@ fn handle_tasks(
if let Some(transform) = future::block_on(future::poll_once(&mut *task)) { if let Some(transform) = future::block_on(future::poll_once(&mut *task)) {
// Add our new PbrBundle of components to our tagged entity // Add our new PbrBundle of components to our tagged entity
commands.entity(entity).insert_bundle(PbrBundle { commands.entity(entity).insert_bundle(PbrBundle {
mesh: box_mesh_handle.0.clone(), mesh: box_mesh_handle.clone(),
material: box_material_handle.0.clone(), material: box_material_handle.clone(),
transform, transform,
..default() ..default()
}); });

View File

@ -15,9 +15,11 @@ fn main() {
.run(); .run();
} }
#[derive(Deref)]
struct StreamReceiver(Receiver<u32>); struct StreamReceiver(Receiver<u32>);
struct StreamEvent(u32); struct StreamEvent(u32);
#[derive(Deref)]
struct LoadedFont(Handle<Font>); struct LoadedFont(Handle<Font>);
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
@ -43,7 +45,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// This system reads from the receiver and sends events to Bevy // This system reads from the receiver and sends events to Bevy
fn read_stream(receiver: ResMut<StreamReceiver>, mut events: EventWriter<StreamEvent>) { fn read_stream(receiver: ResMut<StreamReceiver>, mut events: EventWriter<StreamEvent>) {
for from_stream in receiver.0.try_iter() { for from_stream in receiver.try_iter() {
events.send(StreamEvent(from_stream)); events.send(StreamEvent(from_stream));
} }
} }
@ -54,7 +56,7 @@ fn spawn_text(
loaded_font: Res<LoadedFont>, loaded_font: Res<LoadedFont>,
) { ) {
let text_style = TextStyle { let text_style = TextStyle {
font: loaded_font.0.clone(), font: loaded_font.clone(),
font_size: 20.0, font_size: 20.0,
color: Color::WHITE, color: Color::WHITE,
}; };

View File

@ -20,7 +20,7 @@ enum AppState {
#[derive(Component)] #[derive(Component)]
struct TextToPrint(String); struct TextToPrint(String);
#[derive(Component)] #[derive(Component, Deref, DerefMut)]
struct PrinterTick(bevy::prelude::Timer); struct PrinterTick(bevy::prelude::Timer);
#[derive(Component)] #[derive(Component)]
@ -67,7 +67,7 @@ fn setup_system(mut commands: Commands) {
fn print_text_system(time: Res<Time>, mut query: Query<(&mut PrinterTick, &TextToPrint)>) { fn print_text_system(time: Res<Time>, mut query: Query<(&mut PrinterTick, &TextToPrint)>) {
for (mut timer, text) in query.iter_mut() { for (mut timer, text) in query.iter_mut() {
if timer.0.tick(time.delta()).just_finished() { if timer.tick(time.delta()).just_finished() {
info!("{}", text.0); info!("{}", text.0);
} }
} }

View File

@ -1,7 +1,7 @@
use bevy::{prelude::*, tasks::prelude::*}; use bevy::{prelude::*, tasks::prelude::*};
use rand::random; use rand::random;
#[derive(Component)] #[derive(Component, Deref)]
struct Velocity(Vec2); struct Velocity(Vec2);
fn spawn_system(mut commands: Commands, asset_server: Res<AssetServer>) { fn spawn_system(mut commands: Commands, asset_server: Res<AssetServer>) {
@ -31,7 +31,7 @@ fn move_system(pool: Res<ComputeTaskPool>, mut sprites: Query<(&mut Transform, &
// See the ParallelIterator documentation for more information on when // See the ParallelIterator documentation for more information on when
// to use or not use ParallelIterator over a normal Iterator. // to use or not use ParallelIterator over a normal Iterator.
sprites.par_for_each_mut(&pool, 32, |(mut transform, velocity)| { sprites.par_for_each_mut(&pool, 32, |(mut transform, velocity)| {
transform.translation += velocity.0.extend(0.0); transform.translation += velocity.extend(0.0);
}); });
} }

View File

@ -8,11 +8,12 @@ fn main() {
.run(); .run();
} }
#[derive(Deref)]
struct Message(String); struct Message(String);
// this system produces a Result<usize> output by trying to parse the Message resource // this system produces a Result<usize> output by trying to parse the Message resource
fn parse_message_system(message: Res<Message>) -> Result<usize> { fn parse_message_system(message: Res<Message>) -> Result<usize> {
Ok(message.0.parse::<usize>()?) Ok(message.parse::<usize>()?)
} }
// This system takes a Result<usize> input and either prints the parsed value or the error message // This system takes a Result<usize> input and either prints the parsed value or the error message

View File

@ -10,7 +10,7 @@ fn main() {
.run(); .run();
} }
#[derive(Component)] #[derive(Component, Deref, DerefMut)]
pub struct PrintOnCompletionTimer(Timer); pub struct PrintOnCompletionTimer(Timer);
pub struct Countdown { pub struct Countdown {
@ -44,7 +44,7 @@ fn setup(mut commands: Commands) {
/// using bevy's `Time` resource to get the delta between each update. /// using bevy's `Time` resource to get the delta between each update.
fn print_when_completed(time: Res<Time>, mut query: Query<&mut PrintOnCompletionTimer>) { fn print_when_completed(time: Res<Time>, mut query: Query<&mut PrintOnCompletionTimer>) {
for mut timer in query.iter_mut() { for mut timer in query.iter_mut() {
if timer.0.tick(time.delta()).just_finished() { if timer.tick(time.delta()).just_finished() {
info!("Entity timer just finished"); info!("Entity timer just finished");
} }
} }

View File

@ -76,7 +76,7 @@ struct Paddle;
#[derive(Component)] #[derive(Component)]
struct Ball; struct Ball;
#[derive(Component)] #[derive(Component, Deref, DerefMut)]
struct Velocity(Vec2); struct Velocity(Vec2);
#[derive(Component)] #[derive(Component)]
@ -334,8 +334,8 @@ fn move_paddle(
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) { fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>) {
for (mut transform, velocity) in query.iter_mut() { for (mut transform, velocity) in query.iter_mut() {
transform.translation.x += velocity.0.x * TIME_STEP; transform.translation.x += velocity.x * TIME_STEP;
transform.translation.y += velocity.0.y * TIME_STEP; transform.translation.y += velocity.y * TIME_STEP;
} }
} }
@ -375,21 +375,21 @@ fn check_for_collisions(
// only reflect if the ball's velocity is going in the opposite direction of the // only reflect if the ball's velocity is going in the opposite direction of the
// collision // collision
match collision { match collision {
Collision::Left => reflect_x = ball_velocity.0.x > 0.0, Collision::Left => reflect_x = ball_velocity.x > 0.0,
Collision::Right => reflect_x = ball_velocity.0.x < 0.0, Collision::Right => reflect_x = ball_velocity.x < 0.0,
Collision::Top => reflect_y = ball_velocity.0.y < 0.0, Collision::Top => reflect_y = ball_velocity.y < 0.0,
Collision::Bottom => reflect_y = ball_velocity.0.y > 0.0, Collision::Bottom => reflect_y = ball_velocity.y > 0.0,
Collision::Inside => { /* do nothing */ } Collision::Inside => { /* do nothing */ }
} }
// reflect velocity on the x-axis if we hit something on the x-axis // reflect velocity on the x-axis if we hit something on the x-axis
if reflect_x { if reflect_x {
ball_velocity.0.x = -ball_velocity.0.x; ball_velocity.x = -ball_velocity.x;
} }
// reflect velocity on the y-axis if we hit something on the y-axis // reflect velocity on the y-axis if we hit something on the y-axis
if reflect_y { if reflect_y {
ball_velocity.0.y = -ball_velocity.0.y; ball_velocity.y = -ball_velocity.y;
} }
} }
} }

View File

@ -76,6 +76,7 @@ mod splash {
struct OnSplashScreen; struct OnSplashScreen;
// Newtype to use a `Timer` for this screen as a resource // Newtype to use a `Timer` for this screen as a resource
#[derive(Deref, DerefMut)]
struct SplashTimer(Timer); struct SplashTimer(Timer);
fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) { fn splash_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
@ -104,7 +105,7 @@ mod splash {
time: Res<Time>, time: Res<Time>,
mut timer: ResMut<SplashTimer>, mut timer: ResMut<SplashTimer>,
) { ) {
if timer.0.tick(time.delta()).finished() { if timer.tick(time.delta()).finished() {
game_state.set(GameState::Menu).unwrap(); game_state.set(GameState::Menu).unwrap();
} }
} }
@ -133,6 +134,7 @@ mod game {
#[derive(Component)] #[derive(Component)]
struct OnGameScreen; struct OnGameScreen;
#[derive(Deref, DerefMut)]
struct GameTimer(Timer); struct GameTimer(Timer);
fn game_setup( fn game_setup(
@ -227,7 +229,7 @@ mod game {
mut game_state: ResMut<State<GameState>>, mut game_state: ResMut<State<GameState>>,
mut timer: ResMut<GameTimer>, mut timer: ResMut<GameTimer>,
) { ) {
if timer.0.tick(time.delta()).finished() { if timer.tick(time.delta()).finished() {
game_state.set(GameState::Menu).unwrap(); game_state.set(GameState::Menu).unwrap();
} }
} }

View File

@ -75,11 +75,12 @@ impl Plugin for GameOfLifeComputePlugin {
} }
} }
#[derive(Deref)]
struct GameOfLifeImage(Handle<Image>); struct GameOfLifeImage(Handle<Image>);
struct GameOfLifeImageBindGroup(BindGroup); struct GameOfLifeImageBindGroup(BindGroup);
fn extract_game_of_life_image(mut commands: Commands, image: Res<GameOfLifeImage>) { fn extract_game_of_life_image(mut commands: Commands, image: Res<GameOfLifeImage>) {
commands.insert_resource(GameOfLifeImage(image.0.clone())); commands.insert_resource(GameOfLifeImage(image.clone()));
} }
fn queue_bind_group( fn queue_bind_group(

View File

@ -62,7 +62,7 @@ fn setup(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
}); });
} }
#[derive(Component)] #[derive(Component, Deref)]
struct InstanceMaterialData(Vec<InstanceData>); struct InstanceMaterialData(Vec<InstanceData>);
impl ExtractComponent for InstanceMaterialData { impl ExtractComponent for InstanceMaterialData {
type Query = &'static InstanceMaterialData; type Query = &'static InstanceMaterialData;
@ -151,12 +151,12 @@ fn prepare_instance_buffers(
for (entity, instance_data) in query.iter() { for (entity, instance_data) in query.iter() {
let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
label: Some("instance data buffer"), label: Some("instance data buffer"),
contents: bytemuck::cast_slice(instance_data.0.as_slice()), contents: bytemuck::cast_slice(instance_data.as_slice()),
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST, usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
}); });
commands.entity(entity).insert(InstanceBuffer { commands.entity(entity).insert(InstanceBuffer {
buffer, buffer,
length: instance_data.0.len(), length: instance_data.len(),
}); });
} }
} }

View File

@ -73,7 +73,7 @@ fn scheduled_spawner(
&windows, &windows,
&mut counter, &mut counter,
scheduled.per_wave, scheduled.per_wave,
bird_texture.0.clone_weak(), bird_texture.clone_weak(),
); );
let mut rng = thread_rng(); let mut rng = thread_rng();
@ -82,6 +82,7 @@ fn scheduled_spawner(
} }
} }
#[derive(Deref)]
struct BirdTexture(Handle<Image>); struct BirdTexture(Handle<Image>);
#[derive(Component)] #[derive(Component)]
@ -177,7 +178,7 @@ fn mouse_handler(
&windows, &windows,
&mut counter, &mut counter,
spawn_count, spawn_count,
bird_texture.0.clone_weak(), bird_texture.clone_weak(),
); );
} }
} }