Skip to content
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

RFC: expose-fn-type #3476

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
269 changes: 269 additions & 0 deletions text/3476-expose-fn-type.md
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 marked this conversation as resolved.
Show resolved Hide resolved

### 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 impl Trait for [FunctionType] use-case.

Maybe it would make sense to restrict the RFC to just figuring out the syntax for fn items? Or make a new RFC for just the syntax? I would be willing to drive that effort, if you think so?

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:

  • Coherence: what traits should you be allowed to implement for your function? Can I implement IntoIterator for my function? What if 10 years down the road the language wants to start implementing IntoIterator for function items of the form fn() -> impl Iterator?
  • How might it affect inference rules if you implement a trait for your function, where the trait is also implemented for fn pointers.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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...

Copy link
Contributor

Choose a reason for hiding this comment

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

Note to self: The standard library uses the macro impl_fn_for_zst! to support exactly the use-case of naming a function item, because they store it in generic helper structs, and would like that storage to be zero-cost.

[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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 foo<T>, no preceding fn. For associated functions, MyType<T>::my_function<U> should suffice.


Note that this doesn't solve impl Trait, but as said, you already can't use that in structs, so whatever solution is chosen there could just be retrofitted to apply here.

Copy link
Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
Copy link

Choose a reason for hiding this comment

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

Do we use turbofish syntax if MyStruct takes a generic parameter? e.g.

struct MyStruct<T>(T);

Do we write fn MyStruct<T>::new or fn MyStruct::<T>::new? If we choose the latter, would it cause any inconsistency from the fn send<T> syntax below (as opposed to fn send::<T>)?

Copy link
Author

Choose a reason for hiding this comment

The 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
impl<T> fn MyStruct[::]<T>::new
or is it
impl<T> fn MyStruct::new[::]<T>
?
what do you think? is there a prefered situation for this in rust or should we maybe allow both? @SOF3

Copy link

Choose a reason for hiding this comment

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

definitely the former. MyStruct::new<T> doesn't make sense because the type parameter is on MyStruct not the associated function. This would go wrong if new itself also accepts type parameters.

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.

Copy link
Author

Choose a reason for hiding this comment

The 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 MyStruct::<T>::new instead of MyStruct<T>::new (https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=316deb57a2d9552d8284a35fb56db2a0)

What do you think?

Copy link

Choose a reason for hiding this comment

The 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 (<), but we don't have such ambiguity if we specify that fn is always followed by a path instead of an expression.

Of course, you might also want an expression there if it is a generic typeof operator, but this is not the case for the scope of this RFC.

Copy link

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah this should definitly be forbidden. If we do the {} syntax this would be fn {fn {foo}::bar}. Otherwise I'm currently not sure if we would want fn (fn foo)::bar or fn <fn foo>::bar. I'll leave this open until we found a solution (maybe also for the {} syntax)

/* ... */
}
```
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
Copy link

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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.
Copy link

Choose a reason for hiding this comment

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

Not familiar with compiler parsing internals, but could also consider fn {path}, which is slightly similar to the current compiler diagnostics.

Copy link
Author

Choose a reason for hiding this comment

The 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 {} wrapped around?

Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

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

Consider including consistency of display names as well. Currently:

  • std::any::type_name_of_val(&drop::<()>) evaluates to "core::mem::drop<()>"
  • When a specific function pointer appears in a compiler error, it looks like this:
5 |     let () = drop::<()>;
  |         ^^   ---------- this expression has type `fn(()) {std::mem::drop::<()>}`
  |         |
  |         expected fn item, found `()`

Choices of display include "fn item" (as opposed to "fn pointer" if it is cast to fn(()) first), the function path and a mix of function pointer + {path}. It might be more consistent if the syntax eventually adopted in this RFC is consistent with the syntax in compiler diagnostics.

Copy link
Author

Choose a reason for hiding this comment

The 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 impl Trait type param" problem...
So that we write

fn my_fn(x: impl ToString) -> String { x.to_string() }
impl<X: ToString> fn(X) my_fn {
   // ...
}

Ok that would be really cool.
So would you agree that boilerplate is okay here? @SOF3

"Problem" is that this RFC tends to turn into a more generic type_of thing where this syntax wouldn't be possible anymore.. but type_of is another story so what you requested might be the solution i needed

Copy link
Author

Choose a reason for hiding this comment

The 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 fn_name rather than {fn_name} because i think this could collide when we want to do something like this

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 fn() {name} {block} and fn() {block}

Copy link

Choose a reason for hiding this comment

The 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 impl Trait type param" problem... So that we write

fn my_fn(x: impl ToString) -> String { x.to_string() }
impl<X: ToString> fn(X) my_fn {
   // ...
}

Ok that would be really cool. So would you agree that boilerplate is okay here? @SOF3

"Problem" is that this RFC tends to turn into a more generic type_of thing where this syntax wouldn't be possible anymore.. but type_of is another story so what you requested might be the solution i needed

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.

Copy link

@SOF3 SOF3 Aug 25, 2023

Choose a reason for hiding this comment

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

what about just fn {fn_name}? or fn fn_name(Args)->Return

Copy link
Author

Choose a reason for hiding this comment

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

I don't think that's a problem.

Copy link
Author

Choose a reason for hiding this comment

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

So what about moving the name front? fn my_func and fn my_func(prms) -> ret. But that would not match with the error syntax u sent

Copy link

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

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

So is the syntax that I currently have in the RFC ok?

Copy link
Author

Choose a reason for hiding this comment

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

I added a ToDo's section at the end where i proposed a syntax change but I'm not sure if it is that well with the generic in the for "structure"? 😅