-
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 all 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,269 @@ | ||
- 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 function type of a function item to the user. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
### DasLixou | ||
|
||
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 item, for example, with a trait. | ||
|
||
### madsmtm | ||
|
||
In Swift, some functions have an associated selector that you can access with [`#selector`](https://developer.apple.com/documentation/swift/using-objective-c-runtime-features-in-swift). | ||
|
||
In my crate `objc2`, it would be immensely beautiful (and useful) to be able to do something similar, e.g. access a function's selector using something like `MyClass::my_function::Selector` or `selector(MyClass::my_function)`, instead of having to know the selector name (which might be something completely different than the function name). | ||
|
||
# Terminology | ||
|
||
I'll may shorten `function` to `fn` sometimes. | ||
|
||
- **function pointer**: pointer type with the type syntax `fn(?) -> ?` directly pointing at a function, not the type implementing the `Fn[Once/Mut](?) -> ?` traits. | ||
- **function item** (or just function): a declared function in code. free-standing or associated to a type. | ||
- **function group**: many non-specific functions with the same signature (params, return type, etc.) | ||
- **function trait(s)**: the `Fn[Once/Mut](?) -> ?` traits | ||
- **function type**: the type behind a function, which also implements the function traits. | ||
- **fixed type**: directly named type, no generic / `impl Trait`. | ||
- **describe the function type**: write `fn(..) -> ? name` instead of just `fn name`. | ||
|
||
# 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. | ||
Refering to the hidden type is achieved via the following syntax | ||
```rust | ||
fn is_positive(a: i32) -> bool { /* ... */ } | ||
impl MyTrait for fn(i32) -> bool is_positive { | ||
/* ... */ | ||
} | ||
``` | ||
For function signatures, where every parameter/return-type is a fixed type and can be known just by refering to the function (so no generics or `impl Trait` parameters/return type), we can drop the redundant information: | ||
```rust | ||
fn is_positive(a: i32) -> bool { /* ... */ } | ||
impl MyTrait for fn is_positive { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
> 💡 NOTE: Even when we need to describe the function type but the return type is `()`, we can (just as for function pointers and function traits) drop the `-> ()` from the type. (This should also be added as a lint). | ||
|
||
--- | ||
A function with a more complex signature, like a function that specifies `const`, `unsafe` or `extern "ABI"`, we just ignore that when naming the type: | ||
```rust | ||
const fn my_fn(a: i32) -> (i16, i16) { .. } | ||
impl MyTrait for fn my_fn {} | ||
// or with explicit declaration | ||
impl MyTrait for fn(i32) -> (i16, i16) my_fn { .. } | ||
``` | ||
|
||
When having an async function, we in theory have a `impl Future<Output = ..>` as a return type, which should force us to explicitly declare the function type like so | ||
```rust | ||
async fn request_name(id: PersonID) -> String { .. } | ||
|
||
impl<F: Future<Output = String>> Requestable for fn(PersonID) -> F request_name { | ||
/* ... */ | ||
} | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
We can take a shortcut and use the `async` keyword, as long as the `Output` assoc type in the Future is still fixed | ||
```rust | ||
async fn request_name(id: PersonID) -> String { .. } | ||
|
||
impl Requestable for async 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 `[async] 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() { .. } | ||
} | ||
trait MyTrait {} | ||
impl MyTrait for fn sub::sub_mod_fn { | ||
/* ... */ | ||
} | ||
DasLixou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
> ⚠️ NOTE: The same rules apply here as for normal types. Either the function item or the trait to implement mustn't be foreign for the impl. [Same as E0210](https://github.com/rust-lang/rust/blob/master/compiler/rustc_error_codes/src/error_codes/E0210.md) | ||
|
||
--- | ||
|
||
It should be also possible to get the type of associated functions: | ||
|
||
```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 the associated function comes from a trait, the same rules as for associated types apply here ([Ambiguous Associated Type, E0223](https://github.com/rust-lang/rust/blob/master/compiler/rustc_error_codes/src/error_codes/E0223.md)): | ||
|
||
```rust | ||
struct MyStruct; | ||
type MyTrait { | ||
fn ambiguous(); | ||
} | ||
impl MyTrait for MyStruct { | ||
fn ambiguous() { } | ||
} | ||
impl fn MyStruct::ambiguous { } // ERROR: ambiguous associated function | ||
// instead: | ||
impl fn <MyStruct as MyTrait>::ambiguous { } // OK | ||
``` | ||
|
||
When the type of the associated function has generics, they will be handles as follows | ||
|
||
```rust | ||
struct MyStruct<T>(T); | ||
impl<T> MyStruct<T> { | ||
fn get() -> T { .. } | ||
} | ||
|
||
impl<T> fn MyStruct<T>::get { } | ||
// or fully described: | ||
impl<T> fn() -> T MyStruct<T>::get { } | ||
``` | ||
|
||
--- | ||
|
||
When a function has generics, the function type is forced to be described, and the generic should be placed at it's desired position: | ||
```rust | ||
fn send<T: Send>(val: T, postal_code: u32) {} | ||
impl<T: Send> ParcelStation for fn(T, u32) send { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
When we have an implicit generic, the same rule applies | ||
```rust | ||
fn implicit_generic(val: impl Clone) -> impl ToString {} | ||
impl<T: Clone, U: ToString> for fn(T) -> U implicit_generic { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
--- | ||
|
||
When functions have lifetimes, they have to be included in the types | ||
```rust | ||
fn log(text: &str) { .. } | ||
impl<'a> Logger for fn(&'a str) log { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
When the lifetime is explicitly defined on the function signature and there's no other rule forcing us to describe the function type, we can take a shortcut as follows | ||
```rust | ||
fn log<'a>(text: &'a str) { .. } // explicit lifetime 'a | ||
impl<'a> Logger for fn<'a> log { | ||
/* ... */ | ||
} | ||
``` | ||
|
||
--- | ||
|
||
Just as structs and enums have the possibility to derive traits to automatically generate code, function type have similar ways via attribute macros: | ||
|
||
```rust | ||
#[debug_signature] | ||
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. | ||
|
||
# Additional ToDo's | ||
|
||
## Change the fn type syntax for consistency | ||
|
||
When we try to compile the current [code snippet](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cba2a1391c3e431a7499c6bf427b350d) | ||
```rust | ||
fn cool<'a, T: Clone>(val: &'a T) -> (i32, bool) { | ||
todo!() | ||
} | ||
|
||
fn main() { | ||
let _a: () = cool; | ||
} | ||
``` | ||
we get following error: | ||
``` | ||
error[E0308]: mismatched types | ||
--> src/main.rs:6:18 | ||
| | ||
6 | let _a: () = cool; | ||
| -- ^^^^ expected `()`, found fn item | ||
| | | ||
| expected due to this | ||
| | ||
= note: expected unit type `()` | ||
found fn item `for<'a> fn(&'a _) -> (i32, bool) {cool::<_>}` | ||
|
||
For more information about this error, try `rustc --explain E0308`. | ||
``` | ||
For consistency, we should change the syntax to `for<'a, T: Clone> fn(&'a T) -> (i32, bool) cool` (I'm not sure if we should put generics in the for) | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
The type behind functions already exists, we just need to expose it to the user. | ||
|
||
# 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. | ||
|
||
# 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.