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 Atoms 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);
}