Idea: Concretizing Function `impl` Return Types

The Problem

If we have two functions with the same concrete signature:

struct Struct0;
fn make_struct_0() -> Struct0 {
    Struct0
}
fn make_struct_0_too() -> Struct0 {
    println!("Making Struct0.");
    Struct0
}

Then, it is perfectly fine to conditionally assign them to a variable:

    let my_fn/* : fn() -> Struct0 */ = match predicate {
        true => make_struct_0,
        false => make_struct_0_too,
    };

However, if the functions return impl T, we are screwed:

trait Trait {}
struct Struct1;
impl Trait for Struct0 {}
impl Trait for Struct1 {}

// `Trait` can instead be Iterator, Future, whatever. You get the idea.
fn make_trait_0() -> impl Trait {
    Struct0
}
fn make_trait_1() -> impl Trait {
    Struct1
}

    let my_fn = match predicate {
        true => make_trait_0,
        false => make_trait_1,
    };

// Compiler:
error[E0308]: `match` arms have incompatible types
41 |       let my_fn = match predicate {
   |  _________________-
42 | |         true => make_trait_0,
   | |                 ------------ this is found to be of type `fn() -> impl Trait {make_trait_0}`
43 | |         false => make_trait_1,
   | |                  ^^^^^^^^^^^^ expected `make_trait_0::{opaque#0}`, found `make_trait_1::{opaque#0}`
44 | |     };
   | |_____- `match` arms have incompatible types
   |
   = note: expected fn item `fn() -> impl Trait {make_trait_0}`
              found fn item `fn() -> impl Trait {make_trait_1}`

This does make sense. If my_fn were to be created, calling it could result in either make_trait_0 or make_trait_1 being called, returning different types.

Current Workaround

To deal with this, to my knowledge, we need to heap allocate and dyn the return values of these functions in some ways, for example:

    // Double `Box`. The type is mandatory because the compiler cannot infer it.
    let my_fn: Box<dyn Fn() -> Box<dyn Trait>> = match predicate {
        true => Box::new(|| Box::new(make_trait_0())),
        false => Box::new(|| Box::new(make_trait_1())),
    };

    // Single `Box` by introducing two functions.
    fn make_dyn_trait_0() -> Box<dyn Trait> {
        Box::new(make_trait_0())
    }
    fn make_dyn_trait_1() -> Box<dyn Trait> {
        Box::new(make_trait_1())
    }
    let my_fn/* : fn() -> Box<dyn Trait> */ = match predicate {
        true => make_dyn_trait_0,
        false => make_dyn_trait_1,
    };

Translating this to async stuffs, we have lots of boxing. Currently, if I am not mistaken, most async runtimes box all their tasks.

The Idea

So, it occurred to me that the compiler must know the concrete return type of the function although it says impl Trait:

fn make_trait_0() -> impl Trait /* Compiler: This is `Struct0` */ { /* … */ }

We just cannot refer to them because we don't have concrete names for them.

So, what if we can concretize the impl return type of functions, say, by using a macro return_type_of!?

    // Concretize return values and put them into enums to avoid boxing.
    enum ConcreteTrait {
        Fn0(return_type_of!(make_trait_0)),
        Fn1(return_type_of!(make_trait_1)),
    }
    fn make_enum_trait_0() -> ConcreteTrait {
        ConcreteTrait::Fn0(make_trait_0())
    }
    fn make_enum_trait_1() -> ConcreteTrait {
        ConcreteTrait::Fn1(make_trait_1())
    }
    let my_fn/* : fn() -> ConcreteTrait */ = match predicate {
        true => make_enum_trait_0,
        false => make_enum_trait_1,
    };

Then, we can call my_fn, and then pattern match on the return value to call methods on them directly, without any boxing.

Obviously, return_type_of! would need internal support from the compiler to work.

Is this a solved problem? Is there any interest in this?

Playground for the code above.

After Notes

After I wrote up the above, Discourse recommended me Idea: syntax for function item types, which leads to Named existentials and impl Trait variable declarations #2071 and Tracking issue for RFC 1861: Extern types #43467 , but they seem to be different things then what I discuss here.

(On mobile and can't provide thorough references or examples but) the two planned features for this are type alias impl trait (TAIT) and return type notation (RTN). As I understand it, the former is close; the latter, less so.

4 Likes

Single box works fine with closures coerced to fn pointers

    // Single `Box`.
    let my_fn: fn() -> Box<dyn Trait> = match predicate {
        true => || Box::new(make_trait_0()),
        false => || Box::new(make_trait_1()),
    };

if you implement the trait for a generic enum, like Either, you don’t need to name any impl Trait types

    // using either
    use either::Either;
    impl<L: Trait, R: Trait> Trait for Either<L, R> {}

    let my_fn/* : fn() -> Either<_, _> */ = match predicate {
        true => || Either::Left(make_trait_0()),
        false => || Either::Right(make_trait_1()),
    };

Rust Playground

4 Likes

Interesting. TAIT would allow type aliases that look exactly the same to represent different types, though. It is much more relaxed than the my idea above and there is no explicitly dealing with different impl Trait types.

Cool. Didn't know closure coercion.

This has nothing to do with Either, though (I was initially bamboozled and thought Either does something special). Nothing to do with the impl<…> Trait for Either<…>, neither. It is simply the compiler inferring the type for a generic enum.

// This works just as well.
    enum ConcreteTrait<A, B> {
        Fn0(A),
        Fn1(B),
    }
    let my_fn: fn() -> ConcreteTrait<_, _> = match predicate {
        true => || ConcreteTrait::Fn0(make_trait_0),
        false => || ConcreteTrait::Fn1(make_trait_1),
    };

However, we just cannot spell out the type of my_fn (because it contains impl Trait). That is to do with TAIT again.

Well, you cannot spell the type of make_trait_0 or make_trait_1 either. I thought this was about conditionally assigning either in a match statement. (And having the result be something fn() -> impl Trait-like, too; hence the importance of the impl<…> Trait for Either<…>.)

No Either of course isn’t special, which is why I say “a generic enum, like `Either”.

1 Like

Yes. I get your point.

Also, one could probably write a macro to make that enum for N different async functions and implement Future on it… (Edit: found enum_dispatch via Implement trait automatically for enum - The Rust Programming Language Forum)

There's also been discussion of having a way for the compiler to do this for you in expressions, often called "enum impl trait":

The idea being then that you'd just be able to write something like

fn pick_impl(b: bool) -> impl Trait {
    enum if b {
        make_trait_0()
    } else {
        make_trait_1()
    }
}

And the compiler would generate an enum automatically that delegated the trait.

(No marker in the signature, because it's no different from if you'd returned Either internally, but a marker in the expression to not regress the usual case where it's better to get an error for mismatched things -- with that error mentioning enum if as a way out, probably.)


The other thing you can do today, if you don't need to return it, is something like this:

let temp_0;
let temp_1;

let my_fn: &dyn Trait = 
    if predicate { temp_0 = make_trait_0(); &temp_0 }
    else{ temp_1 = make_trait_1(); &temp_1 };

to type-erase without needing to Box things.

5 Likes

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