for following code:
let conversation_model =
if lsm { CONVMODEL.lock().await } else {
conv_model_loader()
};
CONVMODEL.lock().await
is MutexGuard<T>
and conv_model_loader()
is just T
I need common interface for those two so I can not copy-paste my code for two situations because it will only differ with this type, anything else is the same.
Edit:
there is code ... (at least what I was trying to do)
let (locked, loaded); // pun not intended
if lsm {
locked = CONVMODEL.lock().await;
} else {
loaded = conv_model_loader();
};
let mut chat_context = CHAT_CONTEXT.lock().await;
task::spawn_blocking(move || {
let conversation_model = if lsm { &*locked } else { &loaded };
but I've fialed becuse of
use of possibly-uninitialized variable: `locked`\nuse of possibly-uninitialized `locked`
So question is really how to have MutexGuard
with interface &T
but use it inside spawn_blocking
and also with #[async_recursion]
Edit:
let (mut locked, mut loaded) = (None, None);
if lsm {
locked = Some( CONVMODEL.lock().await );
} else {
loaded = Some( conv_model_loader() );
};
let mut chat_context = CHAT_CONTEXT.lock().await;
task::spawn_blocking(move || {
let (lock, load);
let conversation_model =
if lsm {
lock = locked.unwrap();
&*lock
} else {
load = loaded.unwrap();
&load
};
following code is working but actually very ugly XD (I wonder if it is possible to simplify this code)
Whenever you have some set of choices for a value, you want to reach for enum
. For example, in Rust we don't do things like let value: T; let is_initialized: bool;
, we do Option<T>
.
You have a choice of two values, either an acquired mutex or a direct value. This is typically called "either", and there is a popular Rust crate containing this type: Either
. For you it might look like:
use either::Either;
let conv_model = if lsm {
Either::Left(CONVMODEL.lock().await)
} else {
Either::Right(conv_model_loader())
};
tokio::task::spawn_blocking(move || {
let conversation_model = match &conv_model {
Either::Left(locked) => locked.deref(),
Either::Right(loaded) => loaded,
};
conversation_model.infer();
});
This type used to live in the standard library, but was removed because it wasn't often used as it's fairly trivial to make a more descriptive domain-specific type. I agree with that, and you might do:
pub enum ConvModelSource {
Locked(MutexGuard<'static, ConvModel>),
Loaded(ConvModel),
}
impl Deref for ConvModelSource {
type Target = ConvModel;
fn deref(&self) -> &Self::Target {
match self {
Self::Locked(guard) => guard.deref(),
Self::Loaded(model) => model,
}
}
}
// ...
let conv_model = if lsm {
ConvModelSource::Locked(CONVMODEL.lock().await)
} else {
ConvModelSource::Loaded(conv_model_loader())
};
tokio::task::spawn_blocking(move || {
conv_model.infer();
});
This is much more expressive, and moves the "how to populate this" away from where it's used.
In the common case you do want to use the simpler approach user4815162342 showed. You will store one of the temporaries, form a reference to it (knowing you just initialized it), and hand that back.
This doesn't work with spawn_blocking
, however. The lifetime of the reference is that of the temporaries - handing such a reference off to a spawned task is a dangling reference.
This is why the error messages (of the form "borrowed value does not live long enough" and "argument requires that locked
is borrowed for 'static
") guided you to go down the path of trying to move locked
and loaded
into the closure to be in their final resting place, then form a reference. Then the reference wouldn't be dangling.
But then this implies you move a possibly-uninitialized value into the closure. Rust does not understand you are using an identical check to see which temporary value is populated. (You could imagine a typo on the second check doing !lsm
and now you're switched up.)
Ultimately, you have to move the source of the value into the spawned task (closure) so that you form references with usable lifetimes. The use of enum is basically codifying your boolean case check into something Rust understands and will unpack naturally.