Nameable fn types and user-defined traits

Currently, there is no syntax for referring to the type of an fn item. So far I think the language has coped well without this, but I'd like to discuss design patterns that could be unlocked if Rust had this feature. I don't have any intention of discussing concrete syntax for naming the type of an fn.

To describe the use case I'm thinking about, I'll start at the other end, where the Fn trait family is used: Higher order functions. I can express that I want some value that implements Fn(), this makes this value callable as a function. Any fn foo() {} will implement Fn() automatically, and can be passed as a value into the higher order function. What makes the Fn family special though, is that it's futile to combine it with other trait bounds: fn higher_order<F: (Fn()) + ArbitraryTrait>(f: F) {}. No value can ever satisfy this kind of bound, because there exists no way to manually implement ArbitraryTrait for fn definitions. The unique fn type exists, but is unnamable.

A concrete use case is a way to bind metadata to functions. For example, imagine an endpoint handler:

fn foobar() {}

impl Endpoint for type#foobar {
    const PATH: &'static str = "/api/foobar";
}

fn add_endpoint<F>(f: Fn() + Endpoint) {}

This would open up new ways to design APIs in Rust, via cleaner derive-like attribute macros that just implement a given trait for a function item. The advantage is that it makes it possible to associate properties with a function at a place in the file which is local to the function definition, instead of where the function is referenced (with e.g. fn add_endpoint<F: Fn()>(path: &str, f: F)).

The question is whether this is a direction people would want Rust to go in. What other possibilities would it open, and what kinds of anti-patterns would likely appear? Are there fundamental issues that would make this hard to implement in the type system?

I have tried to find "prior art" by searching this forum and github issues. I am unaware of any pre-existing tracking issue or concrete proposal for this feature, the issue tracker is full of semi-related issues and I might easily overlook things when searching.

1 Like

I think that another potential solution would be to make implementing the Fn* traits stable. That way you could just have a stuct implement both.

3 Likes

You might want to discuss why tweaking things to use a struct doesn't work, or isn't sufficient for your use case.

For example

fn main() {
    add_endpoint(Foobar)
}

fn foobar() {}

trait Endpoint {
    const PATH: &'static str;
    fn call();
}

struct Foobar;

impl Endpoint for Foobar {
    const PATH: &'static str = "/api/foobar";

    fn call() {
        foobar()
    }
}

fn add_endpoint(_: impl Endpoint) {}

Obviously there's a bit of boilerplate involved but not TOO much.

One limitation is there's not really an ideal way to do that if your endpoints need to take different arguments.

1 Like

Thanks for the feedback, but I really want this thread to be about fn types specifically, and not about workaround solutions to my example.

Somewhat related, type-alias-impl-trait will give the ability to have names for opaque fn-items.

pub type Foo = impl Fn() + Copy;

#[allow(non_upper_case_globals)]
pub static foo: Foo = foo_impl;

fn foo_impl() {}

This doesn't give the ability to add extra trait impls though, since you can never refer to the non-opaque item still (and there is afaik no way to also have the "coerce to fn()" behavior, I wonder if there should be impl Into<fn()> for fn() {foo_impl} generated too).

1 Like

+1 to TAITs being the expected way to name a function type (or RPIT type) if needed, at least insomuch as I understand the current direction.

It's not obvious to me that implementing it on the function type itself is the way to go. For example, it could still emit something like

mod endpoints {
    pub struct foobar;
    impl Endpoint for foobar {
        const PATH: &str = …;
        fn call(r: Request) -> Response { … parent::foobar(…) … }
    }
}

And have people do

add_endpoint(endpoints::foobar);

Which doesn't seem that bad, and also lets people write their own trait implementations not using the macros without needing to wait for implementing Fn to be possible on stable. And to make stateful endpoints that use closures without also needing a way to implement traits for closure types.

So I think I'm far more sympathetic to "it's a pain to use TAITs to get types sometimes; it'd be nice to have sugar for common simple cases" than to sticking random extra impls on function voldemort types.

I think discussing this is unavoidable. Language change discussions are fundamentally about whether the extra complexity from the proposed change is worth making the motivational examples better, compared to the less-convenient ways that already exist.

2 Likes

Impl Trait Initiative Draft RFC: Named function types. I have no idea as to the state of the draft other than it exists.

(Personally not a fan of automatically and conditionally introducing the types/associated types with the same name into the surround namespace, and would be happy with just fn#name, but that's not the main point of the draft.)

2 Likes

My main motivation is a way to "tag" functions. I'm a fan of append-only attribute macros that don't mutate the input syntax, but instead cleanly transform into new sibling top-level items, like #[derive] does. Derive is clean because it (ideally) doesn't clutter the namespace. Because there exists no way to do this for functions, my curiosity is triggered: Is prohibiting this a conscious design decision, or is it just because there hasn't been much demand for a feature like this?

Introducing a new TAIT from a macro invocation is something I'd regard as a more complex and less clean solution - it would make it hard to combine several attributes that all insist on generating the same TAIT.

I see the problem with closures, they will probably not be able to implement anything else than traits from the Fn family. I realize the following argument will probably sound a bit contrived, but: This feature would likely be used when registering callbacks with "framework"-like libraries, which typically (but not necessarily) also would abstract over many possible function signatures. To me this means: The function signature itself is what represents my type: The most precise representation of this type in the type system, is the type of my fn. Since the hypothetical framework would not require any fixed signature, passing a closure might not be a natural thing to do, because I would then have to type-annotate every parameter plus the return type - which doesn't look that good in closure notation (just my opinion!).

While it's true that user-defined traits can't be implemented for individual function item types, they can be implemented for function pointers. I am aware that this doesn't solve the problem though.

trait Endpoint {
    const PATH: &'static str;
}

impl Endpoint for fn() {
    const PATH: &'static str = "/api/foobar";
}

fn add_endpoint(f: impl Fn() + Endpoint) {
    f();
}

fn main() {
    add_endpoint((|| println!("hello")) as fn());
}

Another usecase that I wished was possible several times was being able to name Map<SomeIterator, SomeFixedFunction> as a struct member. I did not want to write a custom iterator type because I used the rayon ParallelIterator implementation.

This could also be solved by stable Fn* traits.

That usecase will be possible with TAIT (and is relatively low overhead today by just storing a Map<SomeIterator, fn(...) -> _>, a single function pointer is pretty small compared to a lot of other iterator state).

I think the most overhead when using fn(...) -> _ is not due to the function pointer but due to missing inlining. But this depends on the usecase and is a bit offtopic here.

I think the advantage of explicitly namable fn types over TAIT is the readability without requiring naming conventions.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.