Search code examples
rustasync-awaitfuturerust-tokio

Can I store an `impl Future` as a concrete type?


tokio::net::TcpStream::connect is an async function, meaning it returns an existential type, impl Future. I would like to store a Vec of these futures in a struct. I've found many questions where someone wants to store multiple different impl Futures in a list, but I only want to store the return type of one. I feel like this should be possible without Box<dyn Future> as I am really only storing a single concrete type, but I cannot figure out how without getting found opaque type errors.


Solution

  • It is possible with the nightly feature min_type_alias_impl_trait. The trick is to create a type alias, and a dummy function from which the compiler can infer a defining use.

    #![feature(min_type_alias_impl_trait)]
    
    use tokio::net::TcpStream;
    use core::future::Future;
    
    type TcpStreamConnectFut = impl Future<Output = std::io::Result<TcpStream>>;
    
    fn __tcp_stream_connect_defining_use() -> TcpStreamConnectFut  {
        TcpStream::connect("127.0.0.1:8080")
    }
    
    struct Foo {
        connection_futs: Vec<TcpStreamConnectFut>,
    }
    

    This compiles, but does not work as expected:

    impl Foo {
        fn push(&mut self) {
            self.connection_futs.push(TcpStream::connect("127.0.0.1:8080"));
        }
    }
    
    error[E0308]: mismatched types
       --> src/lib.rs:18:35
        |
    6   | type TcpStreamConnectFut = impl Future<Output = std::io::Result<TcpStream>>;
        |                            ------------------------------------------------ the expected opaque type
    ...
    18  |         self.connection_futs.push(TcpStream::connect("127.0.0.1:8080"));
        |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
        |
        = note: while checking the return type of the `async fn`
        = note: expected opaque type `impl Future` (opaque type at <src/lib.rs:6:28>)
                   found opaque type `impl Future` (opaque type at </playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.7.1/src/net/tcp/stream.rs:111:56>)
        = help: consider `await`ing on both `Future`s
        = note: distinct uses of `impl Trait` result in different opaque types 
    

    Using the dummy function we created does work:

    impl Foo {
        fn push(&mut self) {
            self.connection_futs.push(__tcp_stream_connect_defining_use());
        }
    }
    

    So we can just create wrapper functions:

    fn tcp_stream_connect<A: ToSocketAddrs>(addr: A) -> TcpStreamConnectFut  {
        TcpStream::connect(addr)
    }
    

    Except...

    error: type parameter `A` is part of concrete type but not used in parameter list for the `impl Trait` type alias
      --> src/main.rs:9:74
       |
    9  |   fn tcp_stream_connect<A: ToSocketAddrs>(addr: A) -> TcpStreamConnectFut  {
       |  __________________________________________________________________________^
    10 | |     TcpStream::connect(addr)
    11 | | }
       | |_^
    

    We could just use String or &'static str, and the entire thing compiles:

    type TcpStreamConnectFut = impl Future<Output = std::io::Result<TcpStream>>;
    
    fn tcp_stream_connect(addr: &'static str) -> TcpStreamConnectFut  {
        TcpStream::connect(addr)
    }
    
    struct Foo {
        connection_futs: Vec<TcpStreamConnectFut>,
    }
    
    impl Foo {
        fn push(&mut self) {
            self.connection_futs.push(tcp_stream_connect("..."));
        }
    }
    

    You can also add a generic parameter to the type alias itself, but that probably doesn't make sense in this case:

    type TcpStreamConnectFut<A> = impl Future<Output = std::io::Result<TcpStream>>;
    
    fn tcp_stream_connect<A: ToSocketAddrs>(addr: A) -> TcpStreamConnectFut<A>  {
        TcpStream::connect(addr)
    }
    
    struct Foo {
        connection_futs: Vec<TcpStreamConnectFut<&'static str>>,
    }
    
    impl Foo {
        fn push(&mut self) {
            self.connection_futs.push(tcp_stream_connect("..."));
        }
    }
    

    So it is possible, but there are a couple of restrictions. I'm not sure how many of these are bugs, and how much of it is intentional behavior. There has been discussion about a typeof operator to make this easier, but this is what we've got at the moment.