-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: expose-fn-type
#3476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
RFC: expose-fn-type
#3476
Changes from 8 commits
f43c78b
ae0d137
b2c592b
add0ca5
2f7c0bd
f13f239
7b7aebb
32dfb82
adb2c95
19dba36
fc69e90
9549283
9428e88
55b39fc
9a89cad
0804b52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
- Feature Name: `expose-fn-type` | ||
- Start Date: 2023-08-20 | ||
- RFC PR: [rust-lang/rfcs#3476](https://github.com/rust-lang/rfcs/pull/3476) | ||
- Rust Issue: N/A | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
This exposes the ghost-/inner-/localtype of a function to the user. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
I was trying to make something similar to bevy's system functions. And for safety reasons, they check for conflicts between SystemParams, so that a function requiring `Res<A>` and `ResMut<A>` [panic](https://github.com/bevyengine/bevy/blob/main/crates/bevy_ecs/src/system/system_param.rs#L421). | ||
|
||
Then after I heard about axum's [`#[debug_handler]`](https://docs.rs/axum/latest/axum/attr.debug_handler.html) I wanted to do something similar to my copy of bevy systems, so that I get compile time errors when there is a conflict. I wanted even more, I wanted to force the user to mark the function with a specific proc attribute macro in order to make it possible to pass it into my code and call itself a system. | ||
|
||
For that, I would need to mark the type behind the function, for example, with a trait. | ||
|
||
# Guide-level explanation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I agree that this is where we want to end up, I think that this RFC is focusing too much on the Maybe it would make sense to restrict the RFC to just figuring out the syntax for Implementing traits for functions have many other nuances, I'll just name a few that I don't think have been explored enough in this RFC:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to find some other examples of when you'd want to talk about the function item type instead of just using the function pointer, and I honestly couldn't really find a compelling example, which is supposedly why it doesn't exist yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @madsmtm good point with the fn thing! I honestly wonder how that would integrate with generator functions... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self: The standard library uses the macro |
||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
As we all know, you can refer to a struct by its name and for example implement a trait | ||
```rust | ||
struct Timmy; | ||
impl Person for Timmy { | ||
fn greet() { | ||
println!("Hey it's me, Timmy!"); | ||
} | ||
} | ||
``` | ||
When we want to target a specific function for a trait implementation, we somehow need to get to the type behind it. That is being done with the `fn` keyword as follows | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
```rust | ||
fn my_function() {} | ||
impl MyTrait for fn my_function { | ||
/* ... */ | ||
} | ||
``` | ||
--- | ||
For a better understanding, imagine you have a struct like this: | ||
```rust | ||
struct FnContainer<F: Fn()> { | ||
inner: F, | ||
} | ||
fn goods() { } | ||
|
||
let contained_goods = FnContainer { | ||
inner: goods | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
``` | ||
Here, we make a `FnContainer` which can hold every function with the signature `() -> ()` via generics. | ||
But what about explicitly designing the `FnContainer` for a specific function, just like the compiler does when resolving the generics. This will work the same as with the trait impl from above: | ||
```rust | ||
struct GoodsContainer { | ||
inner: fn goods, | ||
} | ||
fn goods() {} | ||
|
||
let contained_goods = GoodsContainer { | ||
inner: goods, | ||
} | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
--- | ||
A function with a more complex signature, like with parameters, modifiers or a return type, is still just referenced by its name, because it's already unique | ||
```rust | ||
async fn request_name(id: PersonID) -> String { .. } | ||
|
||
impl Requestable for fn request_name { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
# Reference-level explanation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be useful to consider the (imaginary) desugaring of function items into structs. Take for example the following generic function: fn foo<T>(x: T) -> T {
x
} This can roughly be implemented with: #![allow(incomplete_features, non_camel_case_types, non_upper_case_globals)]
#![feature(generic_const_items, fn_traits, unboxed_closures)]
use core::marker::PhantomData;
pub struct foo<T> {
p: PhantomData<T>,
}
// impl Copy, Clone, ...
pub const foo<T>: foo<T> = foo::<T> { p: PhantomData };
impl<T> FnOnce<(T,)> for foo<T> {
type Output = T;
extern "rust-call" fn call_once(self, (x,): (T,)) -> Self::Output {
x
}
}
// + impl Fn, FnMut
// + coercion to `fn`
fn main() {
// Using the function type in various positions
let foo_fn: foo<i32> = foo;
trait Bar {}
impl<T> Bar for foo<T> {}
let _ = foo::<i32>(5);
} This leads me to believe that the syntax for specifying a function item should be much simpler, just Note that this doesn't solve There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wow... that is a lot of thinking going in there... I suppose that you are going to take over the whole scenario with the more general syntax approach? |
||
[reference-level-explanation]: #reference-level-explanation | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it might make sense to explain this relating to the current reference docs on function items, and how we'd update that given that we now have syntax for talking about the function type. |
||
|
||
As described in the **Guide-level explanation**, with the syntax `fn <fn_path>`, we can reference the type behind the named function. | ||
|
||
When the function is for example in a different mod, it should be referenced by its path | ||
```rust | ||
mod sub { | ||
fn sub_mod_fn() { .. } | ||
} | ||
impl fn sub::sub_mod_fn { | ||
/* ... */ | ||
} | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
It should be also possible to get the type of functions inside impl blocks: | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
struct MyStruct; | ||
impl MyStruct { | ||
fn new() -> Self { Self } | ||
} | ||
impl fn MyStruct::new { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we use turbofish syntax if struct MyStruct<T>(T); Do we write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll look into that. I also see another problem here: Imagine this struct MyStruct<T> {
pub fn new(x: T) -> Self {
// ...
}
} is it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. definitely the former. As for whether to use turbofish, I guess this depends on how the compiler parses this expression. I'm not a parser expert, so this part needs some input from the compiler team. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @SOF3 So I made some tests and would say that it's more "rusty" when we do What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, but since we are in type context not expression context here, turbofish is probably unnecessary. The turbofish syntax is required for expressions only because of ambiguity with the less-than operator ( Of course, you might also want an expression there if it is a generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any potential parsing ambiguity if we are taking a fn pointer on an associated function of a fn pointer? e.g. fn foo() {}
impl fn foo {
fn bar() {}
}
// how do we parse this?
type FooBar = fn fn foo::bar; Definitely a very bad syntax that must be disallowed, but better specify it in the reference-level explanation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this should definitly be forbidden. If we do the |
||
/* ... */ | ||
} | ||
``` | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
When a function has generics, they will be handled as follows, just like we know it from normal types | ||
```rust | ||
fn send<T: Send>(val: T) {} | ||
impl<T: Send> ParcelStation for fn send<T> { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
When we have an implicit generic, they will be appended in order to the generic list: | ||
```rust | ||
fn implicit_generic(val: impl Clone) -> impl ToString {} | ||
impl<T: Send, U: ToString> for fn implicit_generic<T, U> { | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/* ... */ | ||
} | ||
``` | ||
|
||
Just as structs and enums have the possibility to derive traits to automatically generate code, function type do too | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
#[derive(DbgSignature)] | ||
fn signature_test(val: i32) -> bool { | ||
/* ... */ | ||
} | ||
|
||
// Expands to | ||
|
||
fn signature_test(val: i32) -> bool { | ||
/* ... */ | ||
} | ||
impl DbgSignature for fn signature_test { | ||
fn dbg_signature() -> &'static str { | ||
"fn signature_test(val: i32) -> bool" | ||
} | ||
} | ||
``` | ||
|
||
Other than that, it should behave like every other type does. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
- When introducing the derive feature, it could lead to parsing problems with proc macros having an older `syn` crate version. | ||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
The type behind functions already exists, we just need to expose it to the user. | ||
The hard part would be allowing derives, because that may break some things. | ||
|
||
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
i dont know any | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are there any existing workarounds for this, e.g. through macros in specific scenarios etc? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. I couldn't imagine how you could get behind a specific type of a function at all. |
||
|
||
# Unresolved questions | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[unresolved-questions]: #unresolved-questions | ||
|
||
- Is the syntax good? It could create confusion between a function pointer. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not familiar with compiler parsing internals, but could also consider There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't that what i already do? or do you mean with explicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
- What about closures? They don't even have names so targetting them would be quite difficult. I wouldn't want to use the compiler generated mess of a name like `[closure@src/main.rs:13:18: 13:20]`. It would also contain line numbers which would be changing quite often so thats not ideal. | ||
- I provided a possible solution for a `fn implicit_generic(val: impl Clone) -> impl ToString` function, but because we currently don't have a defined syntax for those generics in types, thus we can't use `impl Trait` as types for fields in structs, we should think about this more, maybe don't implement exposed types of function for such `fn`s and wait for another RFC? | ||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
- Also expose the type of closures | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider including consistency of display names as well. Currently:
Choices of display include "fn item" (as opposed to "fn pointer" if it is cast to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a fair point, but i feel like that this will just make more boilerplate. we already specified the function and i dont want to make my impl even longer... on the other side: WAIT A MINUTE! this could solve our "what to do with fn my_fn(x: impl ToString) -> String { x.to_string() }
impl<X: ToString> fn(X) my_fn {
// ...
} Ok that would be really cool. "Problem" is that this RFC tends to turn into a more generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So i am currently rewriting and for now I will do this partially but use impl MyTrait for fn() -> bool {
} I don't know if something like this is planned or if this is already marked as "no" (if so please say it to me so ill change it in the RFC), but if it isn't, then the compiler may not be able to differentiate between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
not a fan of this. two type expressions joined together without a delimiter is most likely not acceptable to the compiler team, considering we can't do the same with decl macro inputs either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's a problem. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So what about moving the name front? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually my point was that we should make them consistent, but we could change the diagnostic display instead of the syntax There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is the syntax that I currently have in the RFC ok? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a |
Uh oh!
There was an error while loading. Please reload this page.