Closures and functional traits

TL;DR:

Assume we have following trait and a function that uses it:

trait SingleMethodTrait<T> {
    fn do_stuff<'a>(self, &'a T) -> impl Future<Output = ()> + use<'a>;
}

async fn do_bigger_stuff(
    do_stuff: impl SingleMethodTrait<u32>,
) {
    ...
    let inner_param = 42u32;
    do_stuff.do_stuff(&inner_param).await;
    ...
}

Then, we should be able to provide that trait's implementation by writing closure:

do_bigger_stuff(async |i| {
    println!("{i}");
}).await;

This should work if trait has exactly one non-static method (with any variation of self parameter), all its associated types are referenced in that method's signature (i.e. can be inferred from usage spot), and it's the only non-marker trait in the bound declared.

Rationale

Allow convenient syntax for implementation of any Fn-like trait, with all variations of method signature available. This should also allow higher-kinded functions, as nothing forbids us from methods like fn show_it(&self, item: &impl Debug). Also removes need to make more stdlib traits for functions.

Prior art

Additional possible extensions

  • Syntax or macro to declare such functional trait in-place:
    fn do_stuff(
        func: callable! {
            fn call<'a>(self, &'a mut Foo) -> impl Future<Output = ()> + use<'a>;
        }
    
  • Marker trait Callable which tells compiler that this trait is equivalent to any other marked trait with the same signature of its method

Details

Recently I hit issue with declaring proper async closure type yet again. It was caused by lifetime bounds propagation from arguments to returned future, but that's not what I'd like to talk about.

What I observe is that we're multiplying number of magic traits each time we hit some new limitation. We started with Fn* traits to cover passing functions - yet they couldn't resolve linking closure's lifetime to its output. We added AsyncFn* when we decided to resolve this issue for async functions, for which it was really critical. We're talking about LendingFn* traits family which should land inbetween those two. And all these traits are magical and have special meaning to compiler.

Yet, certain nontrivial cases with callables' signatures are still not covered. Some may be never covered, and all this is because you can't express whole range of possible method signatures with parametrizing traits, at least it seems so now. Rustaceans often workaround this kind of issue with their custom traits, where all the bounds needed are written explicitly. Unfortunately, providing implementation for such traits is often tedious, since you need to write down all the captured variables and their types, esp. if that closure is used only once.

What I'd like to discuss is the ability to generate trait implementation from a closure for any compatible trait.

5 Likes

Prior art should expand on the relation to Java Functional Interfaces, which is very similar to this for a similar use case. Even better it will provide a plethora of additional motivating examples and demonstrate that it has the potential of shifting the design space around it. (That should be discussed as a slight negative, too, for compatibility with previous editions if necessary).

5 Likes

I think something like this might be needed – the basic problem is that there's a sort of "ecosystem split" between Fn traits and closures on one side, and non-standard-library traits and manually-implemented closure-equivalents on the other, and it can be hard to convert when you have one but need the other.

One of the ways I get a deeper understanding of Rust is by answering questions about it on Stack Overflow, and at least two people independently had problems in which they needed to provide a generic callback for which the output type depended on the generic parameters. An approximation of what they were aiming for in psuedo-Rust syntax would look like this:

for<T: Trait1> Fn(T) -> impl Trait2 + use<T>

Obviously, this doesn't work in current Rust – although it's obvious what the syntax would mean by analogy with existing language features, it requires at least two hypothetical features (types as higher-ranked trait bounds and impl Trait return values from Fn) that don't exist and are unlikely to be implemented any time soon.

On the other hand, despite being impossible to write using Fn (and there being no real propsects of being able to do so any time soon), you can write this as a custom trait just fine:

trait MyFn {
    type Output<T: Trait1>: Trait2;
    fn call<T: Trait1>(&self, t: T) -> Self::Output<T>;
}

This is the suggestion that I gave to both of those people, but it has the big downside that you can't create closures that implement MyFn – you have to simulate the closure manually (by creating a struct to hold the captures and implementing MyFn on it) rather than being able to use closure syntax.

As such, I'm in favour of implementing something like this. I would note that to avoid creating a new semver hazard, it should probably be restricted to traits that have exactly one required method (but allow any number of provided methods) – adding a provided method to a trait is normally not a semver break, so allowing provided methods to influence whether it was possible to implement the trait with a closure would be needed to preserve that. It would also need to be able to provide generic associated types (as in the example above), but I think you can do that by making them all TAITs in the generated trait implementation (with the trait's required method as the defining use).

Thanks for suggestion. Added to OP.

That's what I'm talking about. One cannot express all possible function signatures through fixed set of traits. So better generate trait implementation from closure for any compatible trait.

Based on my personal experience with functional interfaces in Java, and with refactoring in Rust and Java, I recommend that this feature should not be implicitly enabled by having only one method; rather, the trait should opt in with an attribute. This has the following benefits:

  • If a trait has one method now but will have more later, it can be ineligible for closure syntax immediately, preventing the need for painful refactoring later. (Yes, adding a method is a breaking change. Not every trait is public in a a library published to crates.io; many are private to large projects which can update the definition and usages together.)

  • In some cases, writing the method name is important documentation about what the method does, and making it anonymous makes the code less clear. (This may be less of a problem for Rust than Java, because Rust does not have anonymous inline class syntax as the option immediately adjacent to closure syntax, and so there is no opportunity for tools to unwisely recommend closure syntax instead.[1])

Both of these points could be addressed by saying "well don't do that then", but I do not think that is wise.


  1. I think it would be useful if Rust did have anonymous inline trait impls too; it would work like a closure but be able to implement a trait with multiple methods. But that's an entire other discussion, with many more syntax questions. ↩︎

6 Likes

IMO trait breakage you described isn't unique to functional interfaces. Any new required method will break all implementations of that trait. This may be partially remedied by allowing any number of provided methods in functional interfaces, though that's a different story. I don't like idea of this feature being opt-in. It will introduce unnecessary limitation for a feature which is essentially a complex syntactic sugar.

Some historical discussion of a related proposed feature (anonymous struct impls): https://github.com/rust-lang/rfcs/pull/2604

3 Likes

Yes, of course. But one is easy and the other is not. Currently, if a new required method is added, the necessary work is to add that method to every implementation — a simple job (other than deciding what the new function should do in each case). But if one of those implementations was using closure syntax, then it must be refactored away from being a closure to being an explicit struct; this may be difficult and irregular.

1 Like

Thank you, I haven't seen that discussion, though I've seen mentions of this approach in the discussion from 2017 I linked.

Do you think full "functional interfaces" RFC has any chances? Unfortunately I'm not quite familiar with current state of affairs in lang team.

1 Like

I skimmed the std's trait listing and I'd say that most single-method traits never make sense to implement as a closure. If it wouldn't make sense to implement a from_fn() function for a trait today, making that conversion implicit likely isn't any more sensible!

1 Like

Defining a closure inline sounds useful, but I'd prefer if it had to be explicit that it's defining a trait implementation instead of just a function. I lean towards reusing as casting (though I'm open to other ideas), maybe something like:

fn takes_iterator<I: Iterator>(iter: I) { .. }
takes_iterator((|| None) as impl Iterator<Item = u32>);

Also making it a cast could allow for named functions to be used, like:

fn returns_none<T>() -> Option<T> { None }
takes_iterator(returns_none::<u32> as impl Iterator<Item = u32>);

AFAIK traits in stdlib are relatively simple, mostly because stdlib is intended to be small. When you get into field of complex signatures, especially involving async, this quickly becomes a problem.

But you don't require such explicit cast in case of normal closures. Neither you do in case of normal type inference. I really don't see any benefit in scattering additional obvious casts everywhere. You also don't need this syntax to use free functions, like you don't need to do so with normal Fn* traits.

I'd really rather if instead of doing this for single-method traits, we'd have some syntax that allows implementing a trait as a closure with any number of methods by naming the methods. Something like (bikeshed syntax):

do_bigger_stuff(impl {
    do_stuff: async |i| {
        println!("{i}");
    },
}).await;
9 Likes

That's fun. Like from_fn in std::iter - Rust for everything :slight_smile:

This is somewhat similiar to Anonymous Classes in Java. And I think this is definitely useful.

Don’t the closure traits, such as Fn, already exist to express the acceptance of closures and function pointers?

Isn't this an issue addressable with macros.

Anyway, if ergonomics is the goal, a Haskell-like lightweight syntax similar to how F# maintains two distinct syntactic styles seems like a better path. That said, I’ve grown tolerant of Rust’s ceremony and punctuation noise.

Those traits cover only quite limited range of possible signatures. One step outside and you need custom trait, with full-blown manual implementation.

The Fn* family of traits does not express everything that can be expressed with a trait with one method. For example, you can define a trait with a generic method:

trait Foo {
    fn foo<T>(&mut self, things: &mut [T]);
}

It’s impossible to express this as a usage of FnMut or to implement this using a from_fn()-style adapter.

3 Likes