Derive macro
In the last section, we were introduced to the Atom
state machine.
If networks were infinitely fast, Atom
might be the only state
machine we ever needed: every time a user changed the state, we'd
just send the new entire state to every other user and be done with it.
In practice, the reason we can't do this is that without infinitely
fast networks, it's possible for different users to make conflicting
changes. For example, let's say we represented a to-do list item
as a state machine by wrapping it in an Atom
:
use aper::data_structures::Atom;
struct ToDoListItem {
done: bool,
label: String,
}
type ToDoListAtom = Atom<ToDoListItem>;
Imagine that you and I are using a shared to-do list app that took this approach to representing state. Suppose we both modified this item at the same time: you marked it as done, and I fixed a typo in the label. Your edit hits the server first, so the server's state is briefly updated with the item marked as done. But my transition was sent at the same time as yours, and because an Atom update carries with it the entire value, it includes the old state of done. When my edit is applied, it has the effect of undoing yours. How rude.
To avoid this, we can push Atom
s deeper down into the data
structure. Aper facilitates this with a derive
macro:
use aper::StateMachine;
use aper::data_structures::{Atom, AtomRc};
use serde::{Serialize, Deserialize};
#[derive(StateMachine, Serialize, Deserialize, Debug, Clone, PartialEq)]
struct ToDoListItem {
done: Atom<bool>,
label: AtomRc<String>,
}
impl ToDoListItem {
pub fn new(label: String) -> Self {
ToDoListItem {
done: Atom::new(false),
label: AtomRc::new(label),
}
}
}
Now, we can represent these same transitions in a way that they don't conflict.
use aper::StateMachine;
use aper::data_structures::{Atom, AtomRc};
use serde::{Serialize, Deserialize};
#[derive(StateMachine, Serialize, Deserialize, Debug, Clone, PartialEq)]
struct ToDoListItem {
done: Atom<bool>,
label: AtomRc<String>,
}
impl ToDoListItem {
pub fn new(label: String) -> Self {
ToDoListItem {
done: Atom::new(false),
label: AtomRc::new(label),
}
}
}
fn main() {
let mut item = ToDoListItem::new("Buy gorceries".to_string());
assert_eq!("Buy gorceries", item.label().value());
assert_eq!(&false, item.done().value());
let mark_done = item.map_done(|d| d.replace(true));
let fix_typo = item.map_label(|d| d.replace("Buy groceries".to_string()));
// Unlike with `ToDoListAtom`, the order in which the transitions
// are applied here does not matter.
item = item.apply(&mark_done).unwrap();
item = item.apply(&fix_typo).unwrap();
assert_eq!("Buy groceries", item.label().value());
assert_eq!(&true, item.done().value());
}
The methods map_done
and map_label
are generated by the derive
macro by prepending map_
to each field. They take a single argument,
which is a function from the type of that field (e.g. Atom<bool>
) to
a type of that field's transition (ReplaceAtom<bool>
). They return a
transition that can be applied to the parent struct (ToDoListItem
),
which combines the transition you constructed in the map with an
indication of which field of the parent struct it is to be applied to.
To better understand this approach, it might be good to understand
what we can't do. For one thing, we can't apply the field's
ReplaceAtom
transition directly to the ToDoListItem
struct that
contains it:
fn main() {
let mut item = ToDoListItem::new("Buy gorceries".to_string());
let mark_done = item.done().replace(true);
// This will fail to compile because `done()` exposes a non-mutable
// borrow of the Atom, but `apply()` requires a mutable borrow.
item.done().apply(mark_done);
}
Nor can we apply the Atom
's state change directly to the
ToDoListItem
:
fn main() {
let mut item = ToDoListItem::new("Buy gorceries".to_string());
let mark_done = item.done().replace(true);
// This will fail to compile because `mark_done` is a
// `ReplaceAtom<bool>` but `item.apply()` expects a
// `&ToDoListItemTransition`.
item = item.apply(&mark_done);
}