Skip to content

[Merged by Bors] - bevy_reflect: Get owned fields#5728

Closed
MrGVSV wants to merge 2 commits intobevyengine:mainfrom
MrGVSV:reflect-drain
Closed

[Merged by Bors] - bevy_reflect: Get owned fields#5728
MrGVSV wants to merge 2 commits intobevyengine:mainfrom
MrGVSV:reflect-drain

Conversation

@MrGVSV
Copy link
Copy Markdown
Member

@MrGVSV MrGVSV commented Aug 18, 2022

Objective

Sometimes it's useful to be able to retrieve all the fields of a container type so that they may be processed separately. With reflection, however, we typically only have access to references.

The only alternative is to "clone" the value using Reflect::clone_value. This, however, returns a Dynamic type in most cases. The solution there would be to use FromReflect instead, but this also has a problem in that it means we need to add FromReflect as an additional bound.

Solution

Add a drain method to all container traits. This returns a Vec<Box<dyn Reflect>> (except for Map which returns Vec<(Box<dyn Reflect>, Box<dyn Reflect>)>).

This allows us to do things a lot simpler. For example, if we finished processing a struct and just need a particular value:

// === OLD === //
/// May or may not return a Dynamic*** value (even if `container` wasn't a `DynamicList`)
fn get_output(container: Box<dyn List>, output_index: usize) -> Box<dyn Reflect> {
  container.get(output_index).unwrap().clone_value()
}

// === NEW === //
/// Returns _exactly_ whatever was in the given list
fn get_output(container: Box<dyn List>, output_index: usize) -> Box<dyn Reflect> {
  container.drain().remove(output_index).unwrap()
}

Discussion

  • Is drain the best method name? It makes sense that it "drains" all the fields and that it consumes the container in the process, but I'm open to alternatives.

Changelog

  • Added a drain method to the following traits:
    • Struct
    • TupleStruct
    • Tuple
    • Array
    • List
    • Map
    • Enum

Changes from initial PR state

Removed drain methods on Struct, TupleStruct, and Enum types as they lacked specific use-cases to validate their inclusion in this PR. They may be added back in at some point in the future.

@MrGVSV MrGVSV added C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types labels Aug 18, 2022
Copy link
Copy Markdown
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on board! Especially for "collection" types like Lists / Arrays / Maps etc.

My only reason to not do this for structs and enums is that it adds more per-type code-gen for something that could be pretty niche. Do you have real use cases for those scenarios? If not, I think we should hold off until absolutely necessary, in the interest of preserving compile time.

@MrGVSV
Copy link
Copy Markdown
Member Author

MrGVSV commented Aug 30, 2022

Do you have real use cases for those scenarios?

I don't really have any actual scenarios. I was just looking at the Struct trait and realized that there was no way for me to get an owned value back from it.

Here's a rather contrived example of when this could be useful:

#[derive(Reflect, FromReflect, Clone, PartialEq, Debug)]
struct Data {
  #[reflect(ignore)]
  value: String
}

#[derive(Reflect, FromReflect, Clone, PartialEq)]
struct SomeStruct {
  data: Data
}

let expected = SomeStruct {
  data: Data {
    value: String::from("Hello World")
  }
};

let dyn_struct: Box<dyn Struct> = Box::new(expected.clone());
// 1.
let field_cloned: Box<dyn Reflect> = dyn_struct.field("data").unwrap().clone_value();
// 2.
let field: Data = <Data as FromReflect>::from_reflect(field_cloned.as_ref()).unwrap();

assert_eq!(expected.data, field); // <- Panic!
// thread 'blah' panicked at 'assertion failed: `(left == right)`
//  left: `Data { value: "Hello World" }`,
// right: `Data { value: "" }`',

This cause of the failure here is due to ignoring Data::value. This causes a problem when we get to the two labeled steps that we need to perform in order to get the owned value.

  1. We call Reflect::clone_value. Because Data is a struct, the returned Box<dyn Reflect> is actually a Box<DynamicStruct>. This is why we need to include step 2.
  2. We then call FromReflect::from_reflect to get back the actual type. Because we ignored Data::value, the derived FromReflect implementation has no idea how to clone that value. So it defaults to using Default::default() to generate the ignored field.

This is why we end up with a blank string and a panicked assertion.

By using something like drain, we can actually take the value directly. No cloning or FromReflect-ing involved.

let dyn_struct: Box<dyn Struct> = Box::new(expected.clone());
let field: Data = dyn_struct.drain().pop().unwrap().take::<Data>().unwrap();
assert_eq!(expected.data, field);

Now, I say this is a contrived example since we (1) already know and have the concrete type of the data we want and (2) we could simply update FromReflect to be smarter (maybe using a #[reflect(cloneable)] attribute).


But yeah— no real-world example on hand. I do see that the compile-time cost could make this part of the PR not really worth it right now (or at least in need of more discussion). I can remove the changes on structs, tuple structs, and enums for the time being.

Copy link
Copy Markdown
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool cool. I see the theoretical value of the struct/enum impls. Happy to re-consider if the need ever shows up, but I appreciate that you removed them in the short term.

This looks good to me!

@cart
Copy link
Copy Markdown
Member

cart commented Aug 30, 2022

bors r+

bors bot pushed a commit that referenced this pull request Aug 30, 2022
# Objective

Sometimes it's useful to be able to retrieve all the fields of a container type so that they may be processed separately. With reflection, however, we typically only have access to references.

The only alternative is to "clone" the value using `Reflect::clone_value`. This, however, returns a Dynamic type in most cases. The solution there would be to use `FromReflect` instead, but this also has a problem in that it means we need to add `FromReflect` as an additional bound.

## Solution

Add a `drain` method to all container traits. This returns a `Vec<Box<dyn Reflect>>` (except for `Map` which returns `Vec<(Box<dyn Reflect>, Box<dyn Reflect>)>`).

This allows us to do things a lot simpler. For example, if we finished processing a struct and just need a particular value:

```rust
// === OLD === //
/// May or may not return a Dynamic*** value (even if `container` wasn't a `DynamicStruct`)
fn get_output(container: Box<dyn Struct>, output_index: usize) -> Box<dyn Reflect> {
  container.field_at(output_index).unwrap().clone_value()
}

// === NEW === //
/// Returns _exactly_ whatever was in the given struct
fn get_output(container: Box<dyn Struct>, output_index: usize) -> Box<dyn Reflect> {
  container.drain().remove(output_index).unwrap()
}
```

### Discussion

* Is `drain` the best method name? It makes sense that it "drains" all the fields and that it consumes the container in the process, but I'm open to alternatives.

---

## Changelog

* Added a `drain` method to the following traits:
  * `Struct`
  * `TupleStruct`
  * `Tuple`
  * `Array`
  * `List`
  * `Map`
  * `Enum`
@bors
Copy link
Copy Markdown
Contributor

bors bot commented Aug 30, 2022

@bors bors bot changed the title bevy_reflect: Get owned fields [Merged by Bors] - bevy_reflect: Get owned fields Aug 30, 2022
@bors bors bot closed this Aug 30, 2022
@MrGVSV MrGVSV deleted the reflect-drain branch September 19, 2022 04:20
james7132 pushed a commit to james7132/bevy that referenced this pull request Oct 28, 2022
# Objective

Sometimes it's useful to be able to retrieve all the fields of a container type so that they may be processed separately. With reflection, however, we typically only have access to references.

The only alternative is to "clone" the value using `Reflect::clone_value`. This, however, returns a Dynamic type in most cases. The solution there would be to use `FromReflect` instead, but this also has a problem in that it means we need to add `FromReflect` as an additional bound.

## Solution

Add a `drain` method to all container traits. This returns a `Vec<Box<dyn Reflect>>` (except for `Map` which returns `Vec<(Box<dyn Reflect>, Box<dyn Reflect>)>`).

This allows us to do things a lot simpler. For example, if we finished processing a struct and just need a particular value:

```rust
// === OLD === //
/// May or may not return a Dynamic*** value (even if `container` wasn't a `DynamicStruct`)
fn get_output(container: Box<dyn Struct>, output_index: usize) -> Box<dyn Reflect> {
  container.field_at(output_index).unwrap().clone_value()
}

// === NEW === //
/// Returns _exactly_ whatever was in the given struct
fn get_output(container: Box<dyn Struct>, output_index: usize) -> Box<dyn Reflect> {
  container.drain().remove(output_index).unwrap()
}
```

### Discussion

* Is `drain` the best method name? It makes sense that it "drains" all the fields and that it consumes the container in the process, but I'm open to alternatives.

---

## Changelog

* Added a `drain` method to the following traits:
  * `Struct`
  * `TupleStruct`
  * `Tuple`
  * `Array`
  * `List`
  * `Map`
  * `Enum`
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
# Objective

Sometimes it's useful to be able to retrieve all the fields of a container type so that they may be processed separately. With reflection, however, we typically only have access to references.

The only alternative is to "clone" the value using `Reflect::clone_value`. This, however, returns a Dynamic type in most cases. The solution there would be to use `FromReflect` instead, but this also has a problem in that it means we need to add `FromReflect` as an additional bound.

## Solution

Add a `drain` method to all container traits. This returns a `Vec<Box<dyn Reflect>>` (except for `Map` which returns `Vec<(Box<dyn Reflect>, Box<dyn Reflect>)>`).

This allows us to do things a lot simpler. For example, if we finished processing a struct and just need a particular value:

```rust
// === OLD === //
/// May or may not return a Dynamic*** value (even if `container` wasn't a `DynamicStruct`)
fn get_output(container: Box<dyn Struct>, output_index: usize) -> Box<dyn Reflect> {
  container.field_at(output_index).unwrap().clone_value()
}

// === NEW === //
/// Returns _exactly_ whatever was in the given struct
fn get_output(container: Box<dyn Struct>, output_index: usize) -> Box<dyn Reflect> {
  container.drain().remove(output_index).unwrap()
}
```

### Discussion

* Is `drain` the best method name? It makes sense that it "drains" all the fields and that it consumes the container in the process, but I'm open to alternatives.

---

## Changelog

* Added a `drain` method to the following traits:
  * `Struct`
  * `TupleStruct`
  * `Tuple`
  * `Array`
  * `List`
  * `Map`
  * `Enum`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Reflection Runtime information about types C-Usability A targeted quality-of-life change that makes Bevy easier to use

Projects

No open projects
Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants