1

Is there a way with Rust to perform the following operation without making models mutable? Possibly by using Stream? The core issue with using uuids.iter().map(...) appears to be (a) passing/moving &mut conn into the closure and (b) the fact that DatabaseModel::load is async.

// assume:
// uuid: Vec<uuid::Uuid>
// conn: &mut PgConnection from `sqlx`

let mut models = Vec::<DatabaseModel>::new();
for uuid in &uuids {
    let model = DatabaseModel::load(conn, uuid).await;
    models.extend(model);
}
//.. do immutable stuff with `models`

A more basic toy example without (a) and (b) above may look like the following, which is closer to what I wish for:

let models = uuids.iter().map(|uuid| DatabaseModel::load(uuid));
Awesome-o
  • 2,002
  • 1
  • 26
  • 38
  • What is the problem with making `models` mutable? You can always make it immutable again after the mutable operations by doing `let models = models;`. – Heiko Ribberink Sep 22 '22 at 09:02
  • I'm not interested in arguments about temporary mutability, I'm simply curious if what I'm hoping for can be achieved. Though my intention is that typical fp operations are less noisy and are clearer about what they intend. – Awesome-o Sep 22 '22 at 18:12
  • 2
    In that case, you could do it with `Stream` by wrapping your `conn` in a `RefCell`, so that you can use it in the closure, and just `.await` the `load` operation. – Heiko Ribberink Sep 23 '22 at 07:53

1 Answers1

2

Yes, what you're looking for is a Stream, a.k.a. "an asynchronous version of Iterator".

You can adapt an existing iterator into a stream by using futures::stream::iter and chain that with .then() to call an async function for each element. Here's an example (playground):

use futures::StreamExt;

let models: Vec<_> = futures::stream::iter(&uuids)
    .then(|uuid| DatabaseModel::load(conn, uuid))
    .collect()
    .await;

However, this won't work if conn is a mutable reference. Streams can't ensure that their futures run purely sequentially. Through the stream its possible to create multiple futures, which would all need the &mut Connection to make progress, but that is not allowed. You would need some form of interior mutability, likely an asynchronous Mutex, to ensure use of conn is exclusive (playground):

use futures::StreamExt;
use tokio::sync::Mutex;

let conn = Mutex::new(conn);
let models: Vec<_> = futures::stream::iter(&uuids)
    .then(|uuid| async {
        let mut conn = conn.lock().await;
        DatabaseModel::load(&mut conn, uuid).await
    })
    .collect()
    .await;

If that is unsavory, then you do need a for-loop with .await to ensure that uses of conn are exclusive. Since that is pretty much set in stone, any other method to create models without mutating it would simply be obtuse.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • 1
    I should have clarified I was also hoping to avoid a Mutex. Maybe the exclusive access I'm really searching for here is a serialized queue that also holds onto the connection. It processes each closure in `.then` 1-by-1 and additionally includes `&mut conn` as an argument to the closure. Since the queue is serialized there's only 1 closure accessing conn at a time. Is there anything like that? (Reminds me somewhat of libdispatch.) – Awesome-o Sep 24 '22 at 05:58