Могу ли я сохранить `impl Future` как конкретный тип?

avatar
Ibraheem Ahmed
1 июля 2021 в 20:40
374
2
3

tokio::net::TcpStream::connect — это асинхронная функция, то есть она возвращает экзистенциальный тип, impl Future. Я хотел бы сохранить Vec этих фьючерсов в структуре. Я нашел много вопросов, когда кто-то хочет сохранить несколько различных impl Future в списке, но я хочу сохранить только возвращаемый тип одного. Я чувствую, что это должно быть возможно без Box<dyn Future>, так как я действительно храню только один конкретный тип, но я не могу понять, как это сделать, не получая ошибок found opaque type.

Источник

Ответы (2)

avatar
Ibraheem Ahmed
1 июля 2021 в 20:40
3

Это возможно с ночной функцией min_type_alias_impl_trait. Хитрость заключается в создании псевдонима типа и фиктивной функции, из которой компилятор может вывести определяющее использование.

#![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> 
}

Это компилируется, но не работает должным образом:

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 

Использование созданной нами фиктивной функции работает:

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

Итак, мы можем просто создать функции-оболочки:

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

Кроме...

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 | | }
   | |_^

Мы могли бы просто использовать String или &'static str, и все это скомпилировалось:

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("..."));
    }
}

Вы также можете добавить универсальный параметр к самому псевдониму типа, но в данном случае это, вероятно, не имеет смысла:

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("..."));
    }
}

Так что это возможно, но есть пара ограничений. Я не уверен, сколько из них являются ошибками, а сколько преднамеренным поведением. Было обсуждение оператора typeof, чтобы сделать это проще, но это то, что у нас есть на данный момент.

avatar
Cryptjar
1 июля 2021 в 22:26
1

Вы можете просто использовать старый добрый Vec, вот так:

use core::future::Future;
use tokio::net::TcpStream;

fn just_vec() -> Vec<impl Future<Output = std::io::Result<TcpStream>>> {
    let mut v = Vec::new();
    
    // connect to several streams
    v.push(TcpStream::connect("127.0.0.1:8080"));
    v.push(TcpStream::connect("127.0.0.2:8080"));
    
    v
}

Однако если вы хотите сохранить его в структуре, становится сложнее, потому что, в отличие от предыдущего случая, где можно вывести конкретный тип, вам нужно быть более явным со структурами.

Одним из стабильных способов сделать это является использование универсальной структуры. На самом деле это очень похоже на сохранение замыкания (где у вас также нет конкретного типа).

use core::future::Future;
use tokio::net::TcpStream;
use tokio::io::AsyncWriteExt;

struct Foo<T> {
    connections: Vec<T> 
}
/// This is just like any other vec-wrapper
impl<T> Foo<T> {
    pub fn new() -> Self {
        Self {
            connections: Vec::new(),
        }
    }
    pub fn push(&mut self, conn: T) {
        self.connections.push(conn);
    }
}
/// Some more specific functios that actually need the Future
impl<T> Foo<T> where T: Future<Output = std::io::Result<TcpStream>> {
    pub async fn broadcast(self, data: &[u8]) -> std::io::Result<()> {
        for stream in self.connections {
            stream.await?.write_all(data).await?
        }
        Ok(())
    }
}

async fn with_struct() -> std::io::Result<()> {
    let mut foo = Foo::new();
    
    // connect to several streams
    foo.push(TcpStream::connect("127.0.0.1:8080"));
    foo.push(TcpStream::connect("127.0.0.2:8080"));
    
    // Do something with the connections
    foo.broadcast(&[1,2,3]).await
}
Ibraheem Ahmed
2 июля 2021 в 02:28
0

Да, это тоже работает, выдвигая работу туда, где тип может быть выведен компилятором. Однако это работает не для всех случаев.