-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
RFC: expose-fn-type
#3476
Conversation
Im unsure about fns of impl blocks. Should this be allowed?: struct MyStruct;
impl MyStruct {
fn new() -> Self { Self }
}
impl Creatable for fn MyStruct::new {
/* ... */
} I have to check whether this applies to the function traits. When the Edit: Checked, those fns still implement Fn trait (as I thought, no reason why not), so I also added an example of how to implement traits for those. |
A type like Just fyi |
I'm aware of that. Fn traits can focus on closures, but also target normal functions. Also this RFC is for implementing a trait on a function directly because something like |
text/3476-impl-trait-for-fn.md
Outdated
Now we can also implement traits for specific functions only | ||
```rust | ||
fn valid() {} | ||
impl ValidFunction for fn valid { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a lot more elaboration on what it actually means. impl
accepts types. Does this mean that fn valid
is now a type referring to the fndef ZST? Can it be used anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, what do you mean by fndef ZST?
And yes, as to my knowledge this is "makes it" a type just as the implementation of Fn
also requires it to be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every function already has an implicit type. So I assume that what you mean is that fn x
is syntax to refer to the type of x
. The RFC should elaborate that, instead of just being around trait impls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every function already has an implicit type. So I assume that what you mean is that
fn x
is syntax to refer to the type ofx
. The RFC should elaborate that, instead of just being around trait impls.
Oh sorry 😅 yes thats exactly what I want. Also: in the unresolved questions tab i wasnt sure if we could drop the fn
in the impl block. But I think it is unique, right? So what do you think about dropping the fn
?
Also: when you say elaborating to the type and not just about trait impls, another question came to my mind: Do we maybe also want to impl directly on a function (I currently work on smth that would really benefit from such a thing)?
Then together with a dropped fn this would look smth like
fn itoa(val: i32) -> String { .. }
impl itoa { .. }
// or without dropping fn
impl fn itoa { .. }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nilstrieb can this be marked as resolved?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically what I'm saying is that "allow impls for function definition types" is the wrong way to approach this. The real feature here is being able to refer to function definition types, so the RFC should focus on that. Mentioning that function definition types are considered local for coherence and can therefore be used in trait impls is then just a small sidenote.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nilstrieb ohhh... thats a great point but I'm not sure if i can rewrite it for it that much because the motivation / goal is more on trait impls directly. But ill try to make it focus more on this point.
Thanks for the feedback 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nilstrieb may you recheck the RFC? I rewrote it but i'm unsure if it's still too focused on the impl thing 😅
You cannot You should ask if const generics could achieve similar, roughly like:
I've no idea if this makes sense either, but at least it resembles topics being discussed elsewhere. There is only one There are many reasons why one would choose an explicit type over an fn or closure, so then invocations look like
|
@burdges I think you got confused by the syntax. When writing |
Afaik there is no ghost type behind the function |
@burdges There is a zero sized type for the function itself too. |
No that's wrong. Closures and functions can have ghost types. |
If some ghost type already exist then whatever, not sure why it'd exist but maybe so. Anyways, that playground link seemingly uses the function pointer type, not some ghost type: You could try printing |
interesting approach... |
@burdges |
Cool. Yes it seems to show a ghost type, not sure why that exists, but maybe it helps in inlining? If you add the line The comment is incorrect though: |
So I added an example for functions with generics, but came across the problem with struct MyStruct {
val: impl MyTrait
} isn't allowed, i don't think we want this to be a quick decision. Thoughts? |
So, historically, I've desired the ability to directly name the types of individual functions before. However, I eventually conceded that type-alias-impl-trait was the solution for this feature, since it seems unclear what the benefit would be. Here, the main use case presented is implementing traits for functions, which… I dunno, I don't really like that. To me, it feels like we should have the inverse of that: allow implementing functions (i.e., |
@clarfonthey so I kind of understand your arguments and I totally agree that we also want the user to be able to implement Fn traits on his own objects, but I think it would be the best to have both. |
@clarfonthey wait... how does type-alias-impl-trat have anything to do with this RFC? |
#![feature(type_alias_impl_trait)]
use std::iter::Map;
type MyFn = impl FnMut(u32) -> u32;
fn square(val: u32) -> u32 {
val * val
}
fn squaring<I: Iterator<Item = u32>>(iter: I) -> Map<I, MyFn> {
iter.map(square)
} |
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
i dont know any |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
# 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 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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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}
There was a problem hiding this comment.
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 writefn 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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"? 😅
impl MyStruct { | ||
fn new() -> Self { Self } | ||
} | ||
impl fn MyStruct::new { |
There was a problem hiding this comment.
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>
)?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
impl MyStruct { | ||
fn new() -> Self { Self } | ||
} | ||
impl fn MyStruct::new { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
# Unresolved questions | ||
[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 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
Co-authored-by: Jonathan Chan Kwan Yin <[email protected]>
Co-authored-by: Jonathan Chan Kwan Yin <[email protected]>
Right, that's why I separated referencing the type of a function from:
Spelled out, I'm proposing that you convert something like this: fn do_something(x: u32) -> u32 { todo!() }
impl MyTrait for fn do_something {
fn my_method(self) -> u32 { todo!() }
} and replace it with: struct MyObject;
impl MyTrait for do_something {
fn my_method(self) -> u32 { todo!() }
fn do_it(i: u32) -> u32 { todo!() }
} If you control |
Co-authored-by: Mads Marquart <[email protected]>
edit: I think a may be wrong here. Type privacy would probably catch this 🤔 It think it's worth mentioning in the RFC that these named fn types could be used to name private return types and private arguments on public functions. /// Workaround for `fn_traits` and/or `unboxed_closures` copied from
/// https://docs.rs/c/latest/src/never_say_never/lib.rs.html#1-77
mod fn_traits {
pub trait FnOnce<Args> {
type Output;
}
impl<F, R> FnOnce<()> for F
where
F: ::core::ops::FnOnce() -> R,
{
type Output = R;
}
}
mod public {
mod private {
pub struct Private;
}
fn returns_private() -> private::Private {
private::Private
}
}
// We get to name `Private` by going through `returns_private`.
// This is only possible if we can name the functions type.
pub type Private = <fn public::returns_private as fn_traits::FnOnce<()>>::Output; Accessing |
Even better: Rust also has a |
@eopb Can public functions even have private types? |
@Aloso Im currently torn back and forth between making this a function-specific thing and a general purpose typeof thing. |
Yes, they can, but I may have been wrong to think that my "trick" would be able to leak types since I think the compiler checks that type aliases don't point to private types. |
Also, i think the lang team is currently working on changing how this works, hopefully making it even more robust. |
@eopb nope. Your problem was that fn was private. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7880db8f7f0e5b217e1bd1ac76fda15b |
@DasLixou no, my function was public to the outer scope while the type was private to that same scope, see https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d9b1f39a040765a5ac5285686e3435cd The compiler is only strict about private types on public functions when the type is very restricted, such as when it's |
the precedence is pretty unclear with the |
But there aren't any operators involved or do I get something wrong? |
any updates on this? |
- **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 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 implementingIntoIterator
for function items of the formfn() -> 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
``` | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation |
There was a problem hiding this comment.
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.
} | ||
``` | ||
|
||
# Reference-level explanation |
There was a problem hiding this comment.
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 struct
s, so whatever solution is chosen there could just be retrofitted to apply here.
There was a problem hiding this comment.
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?
Okay so I just found this draft RFC on named function types, which contains a lot of thoughts that are very similar to what this RFC is proposing. I may open PRs to that repo to merge relevant parts from this RFC text into that. |
I found this while exploring type-handling for automatic differentiation and I'd like to share that use case. Suppose we have a function fn f(x: &[f32], y: &[f32], z: &mut [f32]) {
// ...
} With automatic differentiation, one chooses impl ForwardDiff<(Dual, Const, Dual)> for fn f {
type Tape = Box<[f32]>;
fn augment_forward(x: &[f32], y: &[f32], z: &mut [f32] -> Self::Tape {
// evaluate f(x,y,z) while storing intermediate data to make subsequent differentiation faster
}
fn eval_forward(x: &[f32], dx: &[f32], y: &[f32], z: &mut [f32], dz: &mut [f32], tape: &Self::Tape) {
// note that there is no dy because y's activity is Const
}
} The above could be called like let tape = f.augment_forward(&x, &y, &mut z);
let dx = [1.0, 0.0, 0.0]; // direction in which to differentiate
let mut dz = [0.0; 3];
f.eval_forward(&x, &dx, &y, &mut z, &mut dz, &tape);
// use z, dz Notes:
|
Also, whatever syntax is chosen, I think it's desirable to specify interaction with |
Adds the syntax
fn <fn_path>
to get the exposed type behind a function.Rendered