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
164 changes: 164 additions & 0 deletions text/3476-expose-fn-type.md
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
DasLixou marked this conversation as resolved.
Show resolved Hide resolved

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
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. 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
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 `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 {
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 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
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.
- 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
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"? 😅