rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: `expose-fn-type`

Open DasLixou opened this issue 1 year ago • 47 comments

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

Rendered

DasLixou avatar Aug 20 '23 16:08 DasLixou

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.

DasLixou avatar Aug 22 '23 06:08 DasLixou

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.

burdges avatar Aug 22 '23 12:08 burdges

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.

DasLixou avatar Aug 22 '23 12:08 DasLixou

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(..) -> ..;
}

burdges avatar Aug 22 '23 13:08 burdges

@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 😅

DasLixou avatar Aug 22 '23 13:08 DasLixou

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

burdges avatar Aug 22 '23 13:08 burdges

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

CryZe avatar Aug 22 '23 13:08 CryZe

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

DasLixou avatar Aug 22 '23 13:08 DasLixou

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.

burdges avatar Aug 22 '23 13:08 burdges

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 avatar Aug 22 '23 14:08 DasLixou

@burdges https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=933d56be52fa879eabe2816c1f7da7ce this looks like a ghost type

DasLixou avatar Aug 22 '23 14:08 DasLixou

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.

burdges avatar Aug 22 '23 14:08 burdges

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

Noratrieb avatar Aug 22 '23 14:08 Noratrieb

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?

DasLixou avatar Aug 22 '23 17:08 DasLixou

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.

clarfonthey avatar Aug 23 '23 01:08 clarfonthey

@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 avatar Aug 23 '23 05:08 DasLixou

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

DasLixou avatar Aug 23 '23 05:08 DasLixou

@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)
}

clarfonthey avatar Aug 23 '23 15:08 clarfonthey

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

Note that an alternative to implementing the Fn trait + your trait is to simply add the method they would put as the actual function, as a method on your trait. That's the standard way people have been getting around the lack of this feature, and personally, I find it less confusing than implementing a trait for a function.

Being able to reference the type for a function would be useful if type-alias-impl-trait couldn't do that, but it can, and it's going to be one of the primary uses of it. Compare the above signature to:

fn squaring<I: Iterator<Item = u32>(iter: I) -> Map<I, fn square>;

It's potentially cleaner, but not really enough to warrant the existence of the feature and new syntax, IMHO.

clarfonthey avatar Aug 23 '23 15:08 clarfonthey

@clarfonthey yeah. In that scenario type-alias-impl-trait can represent it and I would also use it over directly using the function. But that's just saying "a part of what this RFC makes possible is already possible so we don't need this". And for some people (like me) it would be really great to get to the type behind a function. If you have concerns over the RFC that it would break something or have feedback, please let me know.

DasLixou avatar Aug 23 '23 16:08 DasLixou

I mean, that's not really how the RFC process works. These features take time and energy to implement, and it's extremely unclear why that energy would be spent toward implementing this feature instead of:

  1. Using type-alias-impl-trait in case you need to directly reference the type for a function (since that feature is actively being developed and would certainly be finished before this feature were even started)
  2. Adding extra methods to traits instead of implementing those traits with fewer methods for functions

Like, sure, this feature as-is wouldn't actually conflict with a lot. But that's only a small bit of the RFC process: the rest is making sure that it fits in with the rest of the language, and is worth implementing. I don't really see many arguments in favour of that.

clarfonthey avatar Aug 24 '23 02:08 clarfonthey

Semantics aside (and I do like the idea of being able to name function types), rather than having a function-specific syntax like fn, this seems like a good argument for having a general type_of!, that works on functions and on anything else.

joshtriplett avatar Aug 24 '23 05:08 joshtriplett

A type_of! macro would also help in turning generics into a variadic function, for size optimizations. I have an open issue (https://github.com/rust-lang/rust/issues/114598) about this, but it's lacking exactly that.

orowith2os avatar Aug 24 '23 06:08 orowith2os

@joshtriplett I also thought of something like type_of keyword instead of fn but didn't know that there is already planning (I guess) for such a macro.

DasLixou avatar Aug 24 '23 08:08 DasLixou

Is this useful? It maybe useful to have traits that declare invariants, like

let my_closure = |..| ..;
unsafe impl SafeforFoo for type_of!(my_closure) { }
something_that_requires_foo(my_closure)

It'd work if my_closure was a bare function too, but would it still be useful if that were the only option? I'd think way less useful than for closures..

I'd wager #![feature(fn_traits)] winds up much more heavily used though, not quite the same as being more useful.

burdges avatar Aug 24 '23 10:08 burdges

@burdges both fn_traits and your example seem quite promising! I totally forgot that you can write an impl everywhere, so that would also eliminate the closure problem. Also: rust has a type keyword reserved, does it have a usecase? Could we write impl SafeForFoo for type my_closure {}?

Edit: I am stupid... type aliasing lol

DasLixou avatar Aug 24 '23 10:08 DasLixou

@orowith2os does there already exist a RFC for type_of?

DasLixou avatar Aug 24 '23 15:08 DasLixou

Could we write impl SafeForFoo for type my_closure {}?

afaict that should work, we can borrow C's sizeof syntax and make type <identifier> and type(<expression>) work as type-of.

programmerjake avatar Aug 24 '23 17:08 programmerjake

Could we write impl SafeForFoo for type my_closure {}?

afaict that should work, we can borrow C's sizeof syntax and make type <identifier> and type(<expression>) work as type-of.

Now the question is what we understand as an . A variable name would be an identifier as well as an expression

DasLixou avatar Aug 24 '23 20:08 DasLixou

Could we write impl SafeForFoo for type my_closure {}?

afaict that should work, we can borrow C's sizeof syntax and make type <identifier> and type(<expression>) work as type-of.

Now the question is what we understand as an . A variable name would be an identifier as well as an expression

yes, that simply means both type my_var and type(my_var) (and type((my_var)) and so on) are all valid. but type a * b would not be since it's missing parenthesis. this is similar to const generic arguments f::<A> or f::<{ A * B }>

programmerjake avatar Aug 24 '23 21:08 programmerjake