Reputation: 12085
I'm trying to figure out how to make this web request code from the yew examples generic for Deserialize types and variants of an enum.
// Deserialize type
#[derive(Debug, Deserialize)]
pub struct TagsResponse {
tags: Vec<String>,
}
// Enum variants
pub enum Msg {
TagsLoaded(Result<TagsResponse, Error>),
TagsLoadError,
}
// Working non-generic inline code
let callback = model.link.send_back(
// want to make TagsResponse generic ⤵
move |response: Response<Json<Result<TagsResponse, Error>>>| {
let (meta, Json(data)) = response.into_parts();
if meta.status.is_success() {
// ↓ and be able to pass in an enum value
Msg::TagsLoaded(data)
} else {
// ↓ and be able to pass in an enum value
Msg::TagsLoadError
}
},
);
let request = Request::get(format!("{}{}", API_ULR, "tags"))
.body(Nothing)
.unwrap();
let task = model.fetch_service.fetch(request, callback);
model.fetch_task.push(task);
Here is about as far as I've gotten which seems pretty close but I've gotten into kind of a loop following the compiler:
fn remote_get<T: 'static>(
fetch_service: &mut FetchService,
link: &mut ComponentLink<Model>,
success_msg: fn(Result<T, Error>) -> Msg,
error_msg: Msg,
) -> FetchTask
where
for<'de> T: serde::Deserialize<'de>,
{
let callback = link.send_back(move |response: Response<Json<Result<T, Error>>>| {
let (meta, Json(data)) = response.into_parts();
if meta.status.is_success() {
success_msg(data)
} else {
error_msg
}
});
let request = Request::get(format!("{}{}", API_ULR, "articles?limit=10&offset=0"))
.body(Nothing)
.unwrap();
fetch_service.fetch(request, callback)
}
with the call site:
let task = remote_get(
&mut self.fetch_service,
&mut self.link,
Msg::TagsLoaded,
Msg::TagsLoadError,
);
self.fetch_task.push(task);
produces:
|
598 | error_msg: Msg,
| --------- captured outer variable
...
608 | error_msg
| ^^^^^^^^^ cannot move out of captured variable in an `Fn` closure
Strangely, if I remove error_msg from the arguments list and simply hard code Msg::TagsLoadError
it will compile but then the request doesn't run. 🤷♂️
Upvotes: 0
Views: 763
Reputation: 98348
ComponentLink::send_back()
expects a Fn
closure. However, your closure is consuming a captured variable, namely error_msg
, so it can only be called once. That makes your closure implement FnOnce
instead of Fn
, so it cannot be used there.
A simpler way to see this is:
struct Foo;
fn call(f: impl Fn() -> Foo) {}
fn test(x: Foo) {
let cb = move || x;
call(cb);
}
The complete error message is somewhat clearer:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
--> src/lib.rs:6:14
|
6 | let cb = move || x;
| ^^^^^^^^-
| | |
| | closure is `FnOnce` because it moves the variable `x` out of its environment
| this closure implements `FnOnce`, not `Fn`
7 | call(cb);
| ---- the requirement to implement `Fn` derives from here
This makes sense; what would happen if you wrote call(cb)
several times? Remember that Foo
is not copyable nor clonable.
Precisely, the easiest solution is to make your type cloneable, so it can be reused:
let cb = move || {
x.clone()
};
And it works!
If you don't want the cost of the clone, you could add some workaround, such as passing a function that returns an error or some kind of reference counted pointer. For example:
struct Foo;
fn call(f: impl Fn() -> Foo) {}
fn test(build_x: impl Fn() -> Foo) {
let cb = move || build_x();
call(cb);
}
This works because build_x
is a Fn
, not a FnOnce
, so it is not consumed when used, that is, you can call it as many times as you want.
Another workaround without callbacks is to use an Option
and consume it by using Option::take
. This replaces it with None
, and from the borrow checker's point of view the value goes on existing. However you need a RefCell
because otherwise you would be mutating a captured variable and converting your closure into a FnMut
.
use std::cell::RefCell;
struct Foo;
fn call(f: impl Fn() -> Foo) {}
fn test(x: Foo) {
let ox = RefCell::new(Some(x));
let cb = move || ox.borrow_mut().take().unwrap();
call(cb);
}
UPDATE to the last option
Do not use a RefCell
when a simple Cell
will do. And Cell
has a take
member function that makes this code simpler:
use std::cell::Cell;
struct Foo;
fn call(f: impl Fn() -> Foo) {}
fn test(x: Foo) {
let ox = Cell::new(Some(x));
let cb = move || ox.take().unwrap();
call(cb);
}
Upvotes: 3