Conditionally calling async functions

Hi everyone :slight_smile: . I would like to implement the following: I have some condition which is decided at runtime and, based on the outcome of this decision, I would like to call one of two asynchronous functions with a similar signature. Something like the following situation:

use std::future::Future;

async fn calling_conditionally(situation: Situation) -> String {
    /// determine which function to call (first compiler error)
    let func = match situation {
        Situation::A => func_a,
        Situation::B => func_b,
    };
    call_correct_function(func).await
}

enum Situation {
    A,
    B,
}

async fn func_a() -> String {
    "doing 'a' stuff".to_owned()
}

async fn func_b() -> String {
    "doing 'b' stuff".to_owned()
}

/// defining a function which takes the function to call as input argument (second compiler error)
async fn call_correct_function<F>(func: F) -> String
where
    F: Fn() -> dyn Future<Output = String>,
{
    func().await
}

With this I get a first compiler error saying that it "expected a future, but found a different future".

... and a second error saying that the "the trait Sized is not implemented for dyn Future<Output = String>".

I understand that I could get the above situation to work by directly calling the appropriate function in the match block of the situation, but would like to understand (a) why the compiler sees the two functions as different types and whether there is a different way of conditionally assigning one of two functions to a variable and (b) how I could write a function such that it takes an asynchronous function as a parameter and calls it.

Thanks a lot! :smiley:

This worked for me but I don't know how strict the function calling requirement is:

use std::future::Future;
use futures::future::BoxFuture;

type FutType = BoxFuture<'static, String>;
// Box<dyn Future<Output = String>

async fn calling_conditionally(situation: Situation) -> String {
    let func: FutType = match situation {
        Situation::A => Box::pin(func_a()),
        Situation::B => Box::pin(func_b()),
    };
    call_correct_function(func).await
}

enum Situation {
    A,
    B,
}

async fn func_a() -> String {
    "doing 'a' stuff".to_owned()
}

async fn func_b() -> String {
    "doing 'b' stuff".to_owned()
}

async fn call_correct_function(fut: FutType) -> String
{
    fut.await
}

It starts turning into a similar problem as trying to return two different closures in a match expression. You can simulate what I'm talking about with this:

async fn calling_conditionally_f(situation: Situation) -> String {
    let func = match situation {
        Situation::A => || Box::pin(func_a()),
        Situation::B => || Box::pin(func_b()),
    };
    call_correct_function(func).await
}

I turned the future into a boxed dyn trait to make it stop trying to unify disparate future types. I used Box::pin to pin the future. BoxedFuture is a convenience that spares you some constraint noise. re: unify disparate types: this wouldn't play nice with the generic type in your initial code either: call_correct_function<F>(func: F). If you use a plain Box<dyn Future<â€Ļ>> without pinning and unsend you won't be able to use the future across an await boundary. To reproduce the issue I'm talking about:

use std::future::Future;
use futures::future::BoxFuture;

// type FutType = BoxFuture<'static, String>;
type FutType = Box<dyn Future<Output = String>>;

async fn calling_conditionally_box(situation: Situation) -> String {
    let func: FutType = match situation {
        Situation::A => Box::new(func_a()),
        Situation::B => Box::new(func_b()),
    };
    call_correct_function(func).await
}

enum Situation {
    A,
    B,
}

async fn func_a() -> String {
    "doing 'a' stuff".to_owned()
}

async fn func_b() -> String {
    "doing 'b' stuff".to_owned()
}

async fn call_correct_function(fut: FutType) -> String
{
    fut.await
}

I'm not sure why you'd need a synchronous Fn() wrapped around a future. The future gives you the ability to suspend computation for later execution sorta like a function. This and scoping/ownership rules around futures and closures are why they have some symmetrical restrictions. In both cases the "concrete" type of a future or closure is unique to the instance and effectively anonymous. impl trait, async fn, and being able to pass boxed trait objects are tools in your toolbox for dealing with that.

1 Like

The reason the compiler sees the two functions as different types is because async function are actually desugared into regular function like this :

async fn foo() -> String {
    // ...
}

fn foo() -> impl Future<Output = String> {
    // ...
}

The compiler will generates an anonymous type that implement the Future trait for each function. This means that two async functions will return a different type (even if both return a String).

4 Likes

As long as func_a and func_b's futures return the same type, you can wrap them in Either::Left and Either::Right from the either crate to get a future with a single type. Note that this requires calling func_a and func_b (to get the Futures) before wrapping them in Either.

use std::future::Future;
use either::Either;

async fn calling_conditionally(situation: Situation) -> String {
    /// determine which function to call
    let fut = match situation {
        Situation::A => Either::Left(func_a()),
        Situation::B => Either::Right(func_b()),
    };
    call_correct_future(fut).await
}

enum Situation {
    A,
    B,
}

async fn func_a() -> String {
    "doing 'a' stuff".to_owned()
}

async fn func_b() -> String {
    "doing 'b' stuff".to_owned()
}

/// defining a function which takes a future to run as input argument
async fn call_correct_future<F>(fut: F) -> String
where
    F: Future<Output = String>,
{
    fut.await
}
3 Likes

For the second one, you might be confusing dyn Trait with generics. dyn Trait is a compiler provided type that you can get by type erasing some other base type which implements the trait Trait. Because different base types with different sizes can be so coerced, dyn Trait is dynamically sized, / "unsized" / does not implement Sized.

Rust doesn't support unsized locals, parameters, or returns, and thus the error. [1] If you wanted to use generics, it might look like so:

async fn call_correct_function<F, R>(func: F) -> String
where
    F: Fn() -> R,
    R: Future<Output = String>,

For the first one, every function has a unique type. But you can generally coerce them to a function pointer, and with normal functions, if they have the same signature they'll coerce to the same function pointer type.

However, async fn is sugar for something notionally similar to this:

// async fn func_a() -> String { /* body */ }
fn func_a() -> impl Future<Output = String> {
    async move { /* body */ }
}

And the impl Future in return position is an opaque type. Every opaque type is unique from any other type. Additionally, even if it weren't opaque, the type of each generated Future type is also unique, just like the type of each closure is unique.

So the outputs of func_a and func_b are different types and function pointers alone won't be able to make the types the same.

You would need type erasure (something like a Pin<Box<dyn Future<Output = String>>>) in order to coerce these to be the same type, and thus something assignable to the same variable / able to be the output of different match arms.

Here's a convoluted example of that. The closures box up the outputs so they can be type erased, and then the closure themselves are coerced to function pointers.


  1. When you work with dyn Trait, you'll typically have a Box<dyn Trait> or &dyn Trait or some other pointer-like type instead. ↩ī¸Ž

3 Likes

And as a follow-up in case it wasn't clear: If you use type erasure in the first part to get either a future or a function that generates a future into a single variable, you have no need of something that works with every possible type of future or every type of future-returning-function in the second part.

Rust is strictly (and statically) typed, so if you end up with something in a variable, it's already been boiled down to a single type. You could just use it.

3 Likes

Oh this is neat :smiley: Thank you!

I think i've got it. Thank you :smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.