Skip to content

Conversation

@MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented Aug 6, 2024

Objective

Note

There are some inaccuracies in this PR and its description regarding the type name of anonymous functions. However, the goal and effectiveness of the PR remains. Please see #14666 for details.

TL;DR

#14098 added the FunctionRegistry but had some last minute complications due to anonymous functions. It ended up going with a "required name" approach to ensure anonymous functions would always have a name.

However, this approach isn't ideal for named functions since, by definition, they will always have a name.

Therefore, this PR aims to modify function reflection such that we can make function registration easier for named functions, while still allowing anonymous functions to be registered as well.

Context

Function registration (#14098) ran into a little problem: anonymous functions.

Anonymous functions, including function pointers, have very non-unique type names. For example, the anonymous function |a: i32, b: i32| a + b has the type name of fn(i32, i32) -> i32. This obviously means we'd conflict with another function like |a: i32, b: i32| a - b.

The solution that #14098 landed on was to always require a name during function registration.

The downside with this is that named functions (e.g. fn add(a: i32, b: i32) -> i32 { a + b }) had to redundantly provide a name. Additionally, manually constructed DynamicFunctions also ran into this ergonomics issue.

I don't entirely know how the function registry will be used, but I have a strong suspicion that most of its registrations will either be named functions or manually constructed DynamicFunctions, with anonymous functions only being used here and there for quick prototyping or adding small functionality.

Why then should the API prioritize the anonymous function use case by always requiring a name during registration?

Telling Functions Apart

Rust doesn't provide a lot of out-of-the-box tools for reflecting functions. One of the biggest hurdles in attempting to solve the problem outlined above would be to somehow tell the different kinds of functions apart.

Let's briefly recap on the categories of functions in Rust:

Category Example
Named function fn add(a: i32, b: i32) -> i32 { a + b }
Closure |a: i32| a + captured_variable
Anonymous function |a: i32, b: i32| a + b
Function pointer fn(i32, i32) -> i32

My first thought was to try and differentiate these categories based on their size. However, we can see that this doesn't quite work:

Category size_of
Named function 0
Closure 0+
Anonymous function 0
Function pointer 8

Not only does this not tell anonymous functions from named ones, but it struggles with pretty much all of them.

My second then was to differentiate based on type name:

Category type_name
Named function foo::bar::baz
Closure foo::bar::{{closure}}
Anonymous function fn() -> String
Function pointer fn() -> String

This is much better. While it can't distinguish between function pointers and anonymous functions, this doesn't matter too much since we only care about whether we can name the function.

So why didn't we implement this in #14098?

Relying on type_name

While this solution was known about while working on #14098, it was left out from that PR due to it being potentially controversial.

The docs for std::any::type_name state:

The returned string must not be considered to be a unique identifier of a type as multiple types may map to the same type name. Similarly, there is no guarantee that all parts of a type will appear in the returned string: for example, lifetime specifiers are currently not included. In addition, the output may change between versions of the compiler.

So that's it then? We can't use type_name?

Well, this statement isn't so much a rule as it is a guideline. And Bevy is no stranger to bending the rules to make things work or to improve ergonomics. Remember that before TypePath, Bevy's scene system was entirely dependent on type_name. Not to mention that type_name is being used as a key into both the TypeRegistry and the FunctionRegistry.

Bevy's practices aside, can we reliably use type_name for this?

My answer would be "yes".

Anonymous functions are anonymous. They have no name. There's nothing Rust could do to give them a name apart from generating a random string of characters. But remember that this is a diagnostic tool, it doesn't make sense to obfuscate the type by randomizing the output. So changing it to be anything other than what it is now is very unlikely.

The only changes that I could potentially see happening are:

  1. Closures replace {{closure}} with the name of their variable
  2. Lifetimes are included in the output

I don't think the first is likely to happen, but if it does then it actually works out in our favor: closures are now named!

The second point is probably the likeliest. However, adding lifetimes doesn't mean we can't still rely on type_name to determine whether or not a function is named. So we should be okay in this case as well.

Solution

Parse the type_name of the function in the TypedFunction impl to determine if the function is named or anonymous.

This once again makes FunctionInfo::name optional. For manual constructions of DynamicFunction, FunctionInfo::named or ``FunctionInfo::anonymous` can be used.

The FunctionRegistry API has also been reworked to account for this change.

FunctionRegistry::register no longer takes a name and instead takes it from the supplied function, returning a FunctionRegistrationError::MissingName error if the name is None. This also doubles as a replacement for the old FunctionRegistry::register_dynamic method, which has been removed.

To handle anonymous functions, a FunctionRegistry::register_with_name method has been added. This works in the same way FunctionRegistry::register used to work before this PR.

The overwriting methods have been updated in a similar manner, with modifications to FunctionRegistry::overwrite_registration, the removal of FunctionRegistry::overwrite_registration_dynamic, and the addition of FunctionRegistry::overwrite_registration_with_name.

This PR also updates the methods on App in a similar way: App::register_function no longer requires a name argument and App::register_function_with_name has been added to handle anonymous functions (and eventually closures).

Testing

You can run the tests locally by running:

cargo test --package bevy_reflect --features functions

Internal Migration Guide

Important

Function reflection was introduced as part of the 0.15 dev cycle. This migration guide was written for developers relying on main during this cycle, and is not a breaking change coming from 0.14.

Note

This list is not exhaustive. It only contains some of the most important changes.

FunctionRegistry::register no longer requires a name string for named functions. Anonymous functions, however, need to be registered using FunctionRegistry::register_with_name.

// BEFORE
registry
  .register(std::any::type_name_of_val(&foo), foo)?
  .register("bar", || println!("Hello world!"));

// AFTER
registry
  .register(foo)?
  .register_with_name("bar", || println!("Hello world!"));

FunctionInfo::name is now optional. Anonymous functions and closures will now have their name set to None by default. Additionally, FunctionInfo::new has been renamed to FunctionInfo::named.

@MrGVSV MrGVSV added C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types X-Contentious There are nontrivial implications that should be thought through D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 6, 2024
@MrGVSV MrGVSV added this to the 0.15 milestone Aug 6, 2024
@MrGVSV MrGVSV force-pushed the mrgvsv/reflect/anonymous-functions branch 2 times, most recently from 560f4b7 to 46f8847 Compare August 6, 2024 22:03
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

I agree with the reasoning in the PR description: if this breaks in the future, it's not vital.

Code quality and docs are impeccable: no notes.

@mweatherley
Copy link
Contributor

Seems like a good compromise in my opinion. Worth mentioning that we also aren't really dependent on type names as unique identifiers, in light of all the business we're doing to accommodate closures and anonymous functions here.

@mweatherley mweatherley added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 7, 2024
@alice-i-cecile
Copy link
Member

@MrGVSV once CI is green I'll merge this in :)

MrGVSV added 2 commits August 6, 2024 19:57
`register` no longer takes a name, `register_dynamic` has been
removed, and `register_with_name` has been added.

Similar changes to the overwriting methods
@MrGVSV MrGVSV force-pushed the mrgvsv/reflect/anonymous-functions branch from 46f8847 to 446622d Compare August 7, 2024 02:57
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Aug 7, 2024
Merged via the queue into bevyengine:main with commit a0cc636 Aug 7, 2024
@MrGVSV MrGVSV deleted the mrgvsv/reflect/anonymous-functions branch August 7, 2024 03:32
github-merge-queue bot pushed a commit that referenced this pull request Aug 8, 2024
…names (#14666)

# Objective

As pointed out by @SkiFire13 on
[Discord](https://discord.com/channels/691052431525675048/1002362493634629796/1270624366119485441),
I was incorrect in #14641 regarding the type name of anonymous
functions. I had stated that they will return something like `fn(i32,
i32) -> i32`, but this is wrong. They actually behave like closures
(despite not technically being closures) and return something more like
`foo::bar::{{closure}}`.

This isn't a major issue because the reasoning behind #14641 still
stands. However, the internal documentation should probably be updated
so future contributors don't believe the lies I left behind.

## Solution

Updated the internal documentation for `create_info` to reflect the
actual type name of an anonymous function.

In that same module, I also added a test for function pointers and
updated all tests to include sanity checks for the `std::any::type_name`
of each category of callable.

## Testing

You can test locally by running:

```
cargo test --package bevy_reflect
```
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 D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants