Can a function takes as input a closure that returns a "impl trait"

Hello Rustineers,
I want to write a function that takes an closure argument, where the closure itself returns a impl trait, sth like this:

trait MatVecMul {}
fn solve<F>(a: Vec<f64>, mul: F) -> f64 
where 
    F: FnOnce(i32) -> impl MatVecMul 
{ ... }

The idea is to allow the user to specify a specific implementation of MatVecMul when they call solve. But it seems this is not allowed by the compiler. It complains:

error[E0562]: impl Trait not allowed outside of function and inherent method return types.

I was wondering what would be the correct way of realizing this idea?

It doesn't have exactly the same semantics as your code (no existential type stuff here), but would it be acceptable to use generics for your use case?

trait MatVecMul {}

fn solve<F, M>(a: Vec<f64>, mul: F) -> f64
where
    F: FnOnce(i32) -> M,
    M: MatVecMul
{ ... }

2 Likes

Actually, that’s almost exactly what the compiler would expand the original code to if it compiled. The problem with the original code is that impl MatVecMul is ambiguous; impl Trait means two different things depending on where it appears:

  • In an argument list, it’s syntax sugar for declaring a type parameter with the given trait bounds.
  • In a return type, it’s an existential type that’s guaranteed to satisfy the listed trait bounds, but is otherwise opaque.

In this case, it appears in both positions and the compiler can’t tell which you intended. The impl is being interpreted as part of the FnOnce return type, where it’s disallowed.


Edit: It looks like this also shows up when trying to return a closure that should have an opaque return type. You can get around that by defining your own trait that represents the properties you want the closure to have:

trait SolveFn: FnOnce(i32) -> <Self as SolveFn>::Result {
    type Result: MatVecMul + Debug;
}

impl<M: MatVecMul + Debug, F: FnOnce(i32) -> M> SolveFn for F {
    type Result = M;
}

fn solve(_: Vec<f64>, mul: impl SolveFn) -> f64 { /* ... */ }
fn make_closure() -> impl SolveFn { /* ... */ }

(Playground)

2 Likes

Curiously this

trait MatVecMul {}
fn solve(a: Vec<f64>, mul: impl FnOnce(i32) -> impl MatVecMul) -> f64 {
    todo!()
}

almost compiles. As in, with nightly, this “desugaring” actually compiles:

#![feature(unboxed_closures)]
trait MatVecMul {}

fn solve(a: Vec<f64>, mul: impl FnOnce<(i32,), Output = impl MatVecMul>) -> f64 {
    todo!()
}
2 Likes

My immediate thought was if putting the full closure or its return in a Box would work. Sounds like there's more to consider though.

I had recently been comparing making a function to return a closure (Box) vs. using the partial macro vs. just accepting that I might have to make the same/similar closure in a few functions to avoid boxes... But cool to see that unboxed closures are being worked on!

This particular example does not demonstrate any features of the unboxed_closures feature other than that it enables the ability to explicitly name the Fn traits by syntax sugar free interface. I.e. writing Fn<(A,B), Output = C> instead of Fn(A, B) -> C. The ability to nest impl like this is readily available on stable and can be used with other traits like e.g. Iterator

fn print_all(it: impl IntoIterator<Item = impl AsRef<[u8]>>) {
    println!("--------------------");
    for slice in it {
        println!("{:?}", slice.as_ref());
    }
}

fn main() {
    let x = vec!["hello".to_string(), "world".to_string()];
    let y = [[1u8, 2, 3], [4, 5, 6]];

    print_all(x);
    print_all(&y);
}

(playground)

It is “only” the fact that impl Trait syntax is not allowed in the return type of a Fn trait that makes this not work. But this is to be expected since those traits are not stabilized yet. As a matter of fact, it might (and in my personal opinion should) be considered to change the closure output type from an associated type into a type parameter, and impl Trait is currently not supported to occur nested in type parameters of another impl Trait, like e.g. x: impl PartialEq<impl Debug>>.


Finally, this function

fn solve(a: Vec<f64>, mul: impl FnOnce<(i32,), Output = impl MatVecMul>) -> f64 {
    todo!()
}

is exactly the same as this one

fn solve<R: MatVecMul>(a: Vec<f64>, mul: impl FnOnce(i32) -> R) -> f64 {
    todo!()
}

or this one

fn solve<R: MatVecMul, F: FnOnce(i32) -> R>(a: Vec<f64>, mul: F) -> f64 {
    todo!()
}

except for the fact that the latter two variants compile successfully on stable and the last one supports explicit passing of the generic type arguments R and F.

1 Like

Thanks a lot. That's what I need. :slight_smile: