Skip to content

Conversation

@MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented Aug 11, 2024

Objective

#14098 added the FunctionRegistry for registering functions such that they can be retrieved by name and used dynamically. One thing we chose to leave out in that initial PR is support for closures.

Why support closures? Mainly, we don't want to prohibit users from injecting environmental data into their registered functions. This allows these functions to not leak their internals to the public API.

For example, let's say we're writing a library crate that allows users to register callbacks for certain actions. We want to perform some actions before invoking the user's callback so we can't just call it directly. We need a closure for this:

registry.register("my_lib::onclick", move |event: ClickEvent| {
    // ...other work...

    user_onclick.call(event); // <-- Captured variable
});

We could have made our callback take a reference to the user's callback. This would remove the need for the closure, but it would change our desired API to place the burden of fetching the correct callback on the caller.

Solution

Modify the FunctionRegistry to store registered functions as DynamicClosure<'static> instead of DynamicFunction (now using IntoClosure instead of IntoFunction).

Due to limitations in Rust and how function reflection works, DynamicClosure<'static> is functionally equivalent to DynamicFunction. And a normal function is considered a subset of closures (it's a closure that doesn't capture anything), so there shouldn't be any difference in usage: all functions that satisfy IntoFunction should satisfy IntoClosure.

This means that the registration API introduced in #14098 should require little-to-no changes on anyone following main.

Closures vs Functions

One consideration here is whether we should keep closures and functions separate.

This PR unifies them into DynamicClosure<'static>, but we can consider splitting them up. The reasons we might want to do so are:

  • Simplifies mental model and terminology (users don't have to understand that functions turn into closures)
  • If Rust ever improves its function model, we may be able to add additional guarantees to DynamicFunction that make it useful to separate the two
  • Adding support for generic functions may be less confusing for users since closures in Rust technically can't be generic

The reasons behind this PR's unification approach are:

  • Reduces the number of methods needed on FunctionRegistry
  • Reduces the number of lookups a user may have to perform (i.e. "get_function or else get_closure")
  • Establishes DynamicClosure<'static> as the de facto dynamic callable (similar to how most APIs in Rust code tend to prefer impl Fn() -> String over fn() -> String)

I'd love to hear feedback on this matter, and whether we should continue with this PR's approach or switch to a split model.

Testing

You can test locally by running:

cargo test --package bevy_reflect

Showcase

Closures can now be registered into the FunctionRegistry:

let punct = String::from("!!!");

registry.register_with_name("my_crate::punctuate", move |text: String| {
  format!("{}{}", text, punct)
});

@MrGVSV MrGVSV added C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 11, 2024
@MrGVSV MrGVSV added this to the 0.15 milestone Aug 11, 2024
@MrGVSV MrGVSV force-pushed the mrgvsv/reflect/closure-registry branch 2 times, most recently from 06759fa to 803d5fa Compare August 11, 2024 01:46
@mweatherley
Copy link
Contributor

In my mind, the changes here to DynamicClosure make DynamicFunction have basically no purpose, since DynamicFunction is now, as far as I can tell, literally isomorphic to DynamicClosure<'static>. I kind of always regarded the function/closure distinction as one without a substantive difference underlying it, so I'm basically in favor of unifying the two in the long run even if we don't do it here.

@MrGVSV
Copy link
Member Author

MrGVSV commented Aug 11, 2024

In my mind, the changes here to DynamicClosure make DynamicFunction have basically no purpose, since DynamicFunction is now, as far as I can tell, literally isomorphic to DynamicClosure<'static>. I kind of always regarded the function/closure distinction as one without a substantive difference underlying it, so I'm basically in favor of unifying the two in the long run even if we don't do it here.

Yeah I think it's really unfortunate we can't get any additional benefits out of DynamicFunction. Theoretically it should be trivially Copy + Send + Sync + ..., but the issue is that IntoFunction needs to move self into the wrapped function.

In fact, I think that's the only major issue wrt improving DynamicFunction. We can make the manual constructor take fn instead of Fn, but we can't utilize that for IntoFunction (or for wrapping fn pointers).

But I think we should save the possible removal/refactor of DynamicFunction for a future PR.

@alice-i-cecile alice-i-cecile 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 15, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Aug 15, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Aug 15, 2024
@alice-i-cecile
Copy link
Member

@MrGVSV MrGVSV force-pushed the mrgvsv/reflect/closure-registry branch from e4a471c to 567058e Compare August 16, 2024 23:56
@MrGVSV
Copy link
Member Author

MrGVSV commented Aug 16, 2024

As an update, once this is merged, I'm going to refactor function reflection to get rid of the DynamicFunction/DynamicClosure issues (relevant Discord thread).

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Aug 17, 2024
Merged via the queue into bevyengine:main with commit 423285c Aug 17, 2024
@MrGVSV MrGVSV deleted the mrgvsv/reflect/closure-registry branch August 17, 2024 05:18
github-merge-queue bot pushed a commit that referenced this pull request Aug 19, 2024
# Objective

One of the changes in #14704 made `DynamicFunction` effectively the same
as `DynamicClosure<'static>`. This change meant that the de facto
function type would likely be `DynamicClosure<'static>` instead of the
intended `DynamicFunction`, since the former is much more flexible.

We _could_ explore ways of making `DynamicFunction` implement `Copy`
using some unsafe code, but it likely wouldn't be worth it. And users
would likely still reach for the convenience of
`DynamicClosure<'static>` over the copy-ability of `DynamicFunction`.

The goal of this PR is to fix this confusion between the two types.

## Solution

Firstly, the `DynamicFunction` type was removed. Again, it was no
different than `DynamicClosure<'static>` so it wasn't a huge deal to
remove.

Secondly, `DynamicClosure<'env>` and `DynamicClosureMut<'env>` were
renamed to `DynamicFunction<'env>` and `DynamicFunctionMut<'env>`,
respectively.

Yes, we still ultimately kept the naming of `DynamicFunction`, but
changed its behavior to that of `DynamicClosure<'env>`. We need a term
to refer to both functions and closures, and "function" was the best
option.


[Originally](https://discord.com/channels/691052431525675048/1002362493634629796/1274091992162242710),
I was going to go with "callable" as the replacement term to encompass
both functions and closures (e.g. `DynamciCallable<'env>`). However, it
was
[suggested](https://discord.com/channels/691052431525675048/1002362493634629796/1274653581777047625)
by @SkiFire13 that the simpler "function" term could be used instead.

While "callable" is perhaps the better umbrella term—being truly
ambiguous over functions and closures— "function" is more familiar, used
more often, easier to discover, and is subjectively just
"better-sounding".

## Testing

Most changes are purely swapping type names or updating documentation,
but you can verify everything still works by running the following
command:

```
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-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants