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
Open

RFC: expose-fn-type #3476

wants to merge 16 commits into from

Conversation

DasLixou
Copy link

@DasLixou DasLixou commented Aug 20, 2023

Adds the syntax fn <fn_path> to get the exposed type behind a function.

Rendered

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 20, 2023
@DasLixou
Copy link
Author

DasLixou commented Aug 22, 2023

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 fn new implements Fn() (I think it is, I just have to make sure), then we probably also want the possibility to implement directly on them, right?

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.

@burdges
Copy link

burdges commented Aug 22, 2023

Fn, FnMut, or FnOnce are closures, not functions. Ain't clear how you'd name the type of a closure, but in theory there is a type there which could satisfy extra traits. And the compiler does them sometimes.

A type like fn(..) -> .. is a function pointer type. I think impl Trait for fn(..) -> .. covers all function with that signature. I doubt impl Trait for fn foo could make sense, because fn bar(..) -> .. { ... } is a value of this function pointer type, not a type itself.

Just fyi #![feature(fn_traits)] permit creating closures manually, but one should typically forgo the function call syntax, and just define the trait you desire.

@DasLixou
Copy link
Author

Fn, FnMut, or FnOnce are closures, not functions. Ain't clear how you'd name the type of a closure, but in theory there is a type there which could satisfy extra traits. And the compiler does them sometimes.

A type like fn(..) -> .. is a function pointer type. I think impl Trait for fn(..) -> .. covers all function with that signature. I doubt impl Trait for fn foo could make sense, because fn bar(..) -> .. { ... } is a value of this function pointer type, not a type itself.

Just fyi #![feature(fn_traits)] permit creating closures manually, but one should typically forgo the function call syntax, and just define the trait you desire.

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 impl<F: Fn(i32) -> bool> MyTrait for F is already possible but not for a single function only.
Pointing at closures is also a thing I mentioned in the "Unresolved Questions" chapter.

Now we can also implement traits for specific functions only
```rust
fn valid() {}
impl ValidFunction for fn valid {
Copy link
Member

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?

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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.

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 { .. }

Copy link
Author

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?

Copy link
Member

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.

Copy link
Author

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 😄

Copy link
Author

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 😅

@burdges
Copy link

burdges commented Aug 22, 2023

You cannot impl Trait for value anywhere in Rust, so doing exactly this sounds impossible.

You should ask if const generics could achieve similar, roughly like:

type F = fn(..) -> ..;

const GOOD_FN: [F] = [ foo, bar, baz ];

impl Trait for F
where GOOD_FN.contains(self)
{
}

I've no idea if this makes sense either, but at least it resembles topics being discussed elsewhere.

There is only one impl Trait for F of course, and F is defined elsewhere, so the orphan rules require Trait be local here, but you could still seal fn parameters like this.

There are many reasons why one would choose an explicit type over an fn or closure, so then invocations look like MyFn::go(..), so you can do whatever you like.

pub trait MyFn {
    fn go(..) -> ..;
}

@DasLixou
Copy link
Author

@burdges I think you got confused by the syntax. When writing impl MyTrait for fn x it doesn't implement MyTrait for the function pointer, rather for the ghosttype behind the function x. Hope that clears things up 😅

@burdges
Copy link

burdges commented Aug 22, 2023

Afaik there is no ghost type behind the function x, only behind closures.

@CryZe
Copy link

CryZe commented Aug 22, 2023

@burdges There is a zero sized type for the function itself too.

@DasLixou
Copy link
Author

Afaik there is no ghost type behind the function x, only one behind closures.

No that's wrong. Closures and functions can have ghost types.
Here's an example where I use the ghost type behind a function which implements the Fn trait:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6789b32b05fcf48f39bc123d3f8c5bdf

@burdges

@burdges
Copy link

burdges commented Aug 22, 2023

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:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=94e692e9d42ed1ad05f068f4777da009

You could try printing type_name to get a final answer there.

@DasLixou
Copy link
Author

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:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=94e692e9d42ed1ad05f068f4777da009

You could try printing type_name to get a final answer there.

interesting approach...

@DasLixou
Copy link
Author

@burdges
Copy link

burdges commented Aug 22, 2023

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 not_really_a_ghost(this_has_a_ghost_type); then you print fn() which is what should happen.

The comment is incorrect though: Fn types are intentionally implemented for function pointers. The surprising thing is that they're also implemented for these ghost types, but maybe this improves inlining or something.

@Noratrieb
Copy link
Member

See https://doc.rust-lang.org/reference/types/function-item.html#function-item-types

@DasLixou DasLixou changed the title RFC: impl-trait-for-fn RFC: expose-fn-type Aug 22, 2023
@DasLixou
Copy link
Author

So I added an example for functions with generics, but came across the problem with impl Trait types.
We could forbid exposing the type of such functions for now but we'll have to get to it later. Particularly when we have derive macros they should be capable of handing functions with impl Trait params/return types.
Or we quickly find a good syntax for it (i just crumbled something up in the RFC where we just add the impl Trait generics in order after the other ones, but because we also don't have a syntax for such things with other types, which I think is also the reason why

struct MyStruct {
    val: impl MyTrait
}

isn't allowed, i don't think we want this to be a quick decision.

Thoughts?

@clarfonthey
Copy link
Contributor

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., FnMut, Fn, and FnOnce) for other kinds of objects. To me, having to implement a trait for a function feels like a hack because other kinds of objects can't be functions.

@DasLixou
Copy link
Author

@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.
I am also currently writing a library where I would need this and having the user to make a zero-typed struct and implementing the Fn trait + my trait vs. Just using a function makes really a difference.

@DasLixou
Copy link
Author

@clarfonthey wait... how does type-alias-impl-trat have anything to do with this RFC?

@clarfonthey
Copy link
Contributor

@clarfonthey wait... how does type-alias-impl-trat have anything to do with this RFC?

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=f704eaba39577e9f9321782fa84afdf5

#![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
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.

# 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"? 😅

text/3476-expose-fn-type.md Outdated Show resolved Hide resolved
text/3476-expose-fn-type.md Outdated Show resolved Hide resolved
text/3476-expose-fn-type.md Show resolved Hide resolved
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.

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.

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)

text/3476-expose-fn-type.md Outdated Show resolved Hide resolved
text/3476-expose-fn-type.md Outdated Show resolved Hide resolved
# Unresolved questions
[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

@clarfonthey
Copy link
Contributor

clarfonthey commented Aug 25, 2023

It seems that you cannot (yet) use TAIT to implement a trait:

Right, that's why I separated referencing the type of a function from:

Adding extra methods to traits instead of implementing those traits with fewer methods for functions

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 MyTrait, rather than asking for someone to implement (FnMut(u32) -> u32) + MyTrait, ask them to just implement MyTrait and add a method to that trait with the appropriate signature.

text/3476-expose-fn-type.md Outdated Show resolved Hide resolved
text/3476-expose-fn-type.md Show resolved Hide resolved
@eopb
Copy link
Contributor

eopb commented Aug 26, 2023

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 Private from the outer scope is not something that's possible today. This isn't something I'd mind having as private return types on public functions can be quite annoying 😅. Fortunately type aliases can't be used as unit struct constructors. That would have made this type leak a much bigger issue.

@Aloso
Copy link

Aloso commented Aug 27, 2023

Also: rust has a type keyword reserved

Even better: Rust also has a typeof keyword reserved 😄

@DasLixou
Copy link
Author

@eopb Can public functions even have private types?

@DasLixou
Copy link
Author

@Aloso Im currently torn back and forth between making this a function-specific thing and a general purpose typeof thing.
But for typeof we would have the problem with "what do we do with (ill call them) implicit generics (e.g. impl Trait)."
In this RFC we have a solution but when making this general typeof thing this won't work anymore.

@eopb
Copy link
Contributor

eopb commented Aug 27, 2023

@eopb Can public functions even have private types?

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.

@madsmtm
Copy link
Contributor

madsmtm commented Aug 27, 2023

Also, i think the lang team is currently working on changing how this works, hopefully making it even more robust.

@DasLixou
Copy link
Author

@eopb nope. Your problem was that fn was private. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7880db8f7f0e5b217e1bd1ac76fda15b
As you can see we error

@eopb
Copy link
Contributor

eopb commented Aug 27, 2023

@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 pub(super)

@SOF3
Copy link

SOF3 commented Aug 28, 2023

the precedence is pretty unclear with the fn xxx syntax. if we don't switch to braces, some specification on the precedence might be necessary.

@DasLixou
Copy link
Author

the precedence is pretty unclear with the fn xxx syntax. if we don't switch to braces, some specification on the precedence might be necessary.

But there aren't any operators involved or do I get something wrong?

@DasLixou
Copy link
Author

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

```

# Reference-level explanation
[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.

}
```

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

@madsmtm
Copy link
Contributor

madsmtm commented Nov 8, 2023

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.

@jedbrown
Copy link

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 Reverse or Forward mode (analogous to left- and right-associativity) and specifies activity for each argument, which leads to a combinatorial number of possible variants. An attractive option would be to be able to do something like the following. (Note that this split mode is mostly used for reverse-mode AD, but it's also relevant to forward-mode, which may be more intuitive for the reader.)

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:

  • ForwardDiff</* activity */> takes activity and it's useful to implement for different combinations (but usually not all combinations). This makes it hard to name regular methods. I'm not concerned here about precise calling syntax and whether we pass something like Dual(&x, &dx) instead of &x, &dx as separate arguments.
  • It's useful to associate derivatives with trait methods (even if not object-safely).
  • In a lot of materials and physics contexts, traits or structs will have many base functions (like f) that need associated derivatives.

@jedbrown
Copy link

Also, whatever syntax is chosen, I think it's desirable to specify interaction with std::any::type_name_of_val so that std::any::type_name_of_val(&f) will return something interpretable as a type (it currently just returns the function name, which is a value).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.