Here is a simplified version of what I'm trying to do:
pub struct EntryManager {
pub entries: Vec<String>,
}
impl EntryManager {
pub fn add_entry(&mut self, text: String) {
self.entries.push(text);
}
}
pub enum EntryManagerAction {
Add(String),
}
impl Reducible for EntryManager {
type Action = EntryManagerAction;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
match action {
// this works
EntryManagerAction::Add(text) => {
let mut manager = (*self).clone();
manager.add_entry(text);
manager.into()
},
// this doesn't (self is immutable, doesn't return Self)
EntryManagerAction::Add(text) => self.add_entry(text),
}
}
}
In my case, this EntryManager
would have many more properties and cloning at every action seems very expensive. It would be very convenient to have a mutable access to EntryManager
within reduce
. Is there another solution or suggestions?
I don't know yew
then I don't know if the self: Rc<Self>
argument of reduce()
is supposed to be the only reference to the EntryManager
at the moment this function is called.
If this is the case, but I really doubt it is, then the V1
variant in the example below could suffice.
It relies on Rc::get_mut()
which checks if there is no other reference to the inner data before allowing the mutation.
But if it is actually shared, the reduction has no effect!
On the other hand, if an EntryManager
is fundamentally shared in this reduction, and I guess it is, because of Rc
, then the only solution is interior mutability (i.e. enable mutation even on shared states, at the cost of a minimal runtime check).
In the V2
variant below, the vector of strings is wrapped inside a RefCell
.
The .borrow_mut()
access performs a cheap runtime check in order to be sure there is no other borrow (exclusive or shared) at the same time (from an upstream operation accessing this vector while triggering reduce()
for example).
Then, the most noticeable difference with V1
is that now add_entry()
of V2
does not require &mut self
anymore (only &self
), which eases dealing with the implicit shared situation implied by Rc
.
use std::{cell::RefCell, rc::Rc};
enum EntryManagerAction {
Add(String),
}
#[derive(Debug)]
struct EntryManagerV1 {
entries: Vec<String>,
}
impl EntryManagerV1 {
fn add_entry(
&mut self,
text: String,
) {
self.entries.push(text);
}
fn simulate_reduce(
mut self: Rc<Self>,
action: EntryManagerAction,
) -> Rc<Self> {
match action {
EntryManagerAction::Add(text) => {
if let Some(em) = Rc::get_mut(&mut self) {
em.add_entry(text);
} else {
println!("!!! EntryManager is shared !!!")
}
self
}
}
}
}
#[derive(Debug)]
struct EntryManagerV2 {
entries: RefCell<Vec<String>>,
}
impl EntryManagerV2 {
fn add_entry(
&self, // no &mut here, thanks to RefCell
text: String,
) {
self.entries.borrow_mut().push(text);
}
fn simulate_reduce(
self: Rc<Self>,
action: EntryManagerAction,
) -> Rc<Self> {
match action {
EntryManagerAction::Add(text) => {
self.add_entry(text);
self
}
}
}
}
fn main() {
{
println!("~~~~ V1 ~~~~");
let em = Rc::new(EntryManagerV1 {
entries: Vec::new(),
});
let em =
em.simulate_reduce(EntryManagerAction::Add("AAA".to_owned()));
println!("first add: {:?}", em);
let em2 = Rc::clone(&em); // shared between two references now!
let em2 =
em2.simulate_reduce(EntryManagerAction::Add("BBB".to_owned()));
println!("second add: {:?}", em2);
}
{
println!("~~~~ V2 ~~~~");
let em = Rc::new(EntryManagerV2 {
entries: RefCell::new(Vec::new()),
});
let em =
em.simulate_reduce(EntryManagerAction::Add("AAA".to_owned()));
println!("first add: {:?}", em);
let em2 = Rc::clone(&em); // shared between two references now!
let em2 =
em2.simulate_reduce(EntryManagerAction::Add("BBB".to_owned()));
println!("second add: {:?}", em2);
}
}
/*
~~~~ V1 ~~~~
first add: EntryManagerV1 { entries: ["AAA"] }
!!! EntryManager is shared !!!
second add: EntryManagerV1 { entries: ["AAA"] }
~~~~ V2 ~~~~
first add: EntryManagerV2 { entries: RefCell { value: ["AAA"] } }
second add: EntryManagerV2 { entries: RefCell { value: ["AAA", "BBB"] } }
*/