Currying multiple arguments

Hi there!

Is there a way to make this playground compile without using Box or any dynamic allocation?

As you can see I’ve tried nesting impl Trait in the return type, which apparently isn’t supported.

Will Rust be capable of this eventually, is there an RFC for this?
Are there fundamental reasons why it’s not possible?

Best,
ambiso

Edit:

I guess this would be a way, but of course that seems quite tedious to write, and I’d rather have the compiler generate this.

1 Like

Not at the moment, with existential types you can do this

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

You can also use the unboxed_closures nightly feature to do this

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

4 Likes

Uhhhh! That’s really cool!

Interesting, why don’t unboxed_closures work with the sugary Fn syntax?

Yes. Take for example this function:

fn foo<T>();

We can take this function as a function pointer fn, but only if we declare the type we will call it with:

//let fn_ptr = foo;
let fn_ptr = foo::<usize>;

One thing that most statically typed languages don’t have is generic function pointers, because how a function pointer usually works is that we just get an address and pass the parameters to it. The problem is that the function behind that address could be outfitted for working with T or U, which is where a plethora of problems come in with just bare function pointers.
Well, moving on , we have the following:

fn foo() -> impl Debug;

Which is to some extent similar to:

fn foo<T: Debug>() -> T;

But the type T must be chosen by the function, so this wouldn’t work because this is chosen by the caller, not the called. But, we can overlook this and still think of it as a kind of generic function. Therefore your initial example becomes something like so:

fn add<T: Fn(i32) -> U, U: Fn(i32) -> i32>() -> T {
    move |a: i32| {
        move |b: i32| {
            a + b
        }
    }
}

Which as we’ve learned, doesn’t work because we have the called (add) choose the type in the body, while the caller choose it at calling time.


To address @KrishnaSannasi’s great unboxed_closures example, the thing here, is that this is more of a “flattened” kind of generic, not so much of a “nested” like the nested impls.

2 Likes

The reason is to maintain consistency with fn() -> impl Trait, which doesn’t work.

The only thing that I did was de-sugar Fn(T) -> U to Fn<(T,) Output = U>, so the nesting doesn’t change.

If I understand correctly, essentially, the generic type of the entire “function tree” must be determined at the initial call site of add() - but this is exactly what I want in this case, right?

@KrishnaSannasi’s point has confused me abit now, why would the desugaring fix this issue…? Are Fn() -> () types not interpreted the same as Fn<I, Output = O> at the compiler and type level?

1 Like

I have the same question.

1 Like

There is some special casing of Fn(T) -> U to disallow Fn(...) -> impl Fn(...) explicitly. I don’t remember where I saw this explained, but I think it was to maintain consistency with fn() -> impl Fn(...). Other than that, it is just desugared to normal trait syntax.

The reason is because we want fn(x: impl Fn() -> impl Trait) to desugar to fn(x: for<T: Trait> impl Fn() -> T), but since we don’t have universal quantification, this doesn’t work. So the syntax has been wholesale disallowed. fn(...) -> impl Trait was disallowed to remain consistent with this.

I found some discussion about it here

and here

5 Likes