Reducing More Boilerplate

Writing code to manually construct a dependency graph, connecting nodes to their dependencies, can quickly become tedious and error-prone, particularly for larger graphs. As shown in the example, each node and connection must be explicitly defined and linked, leading to a significant amount of boilerplate code.

let a = InputNode::new(SomeNumber { value: 1 });
let b = InputNode::new(SomeNumber { value: 2 });
let c = InputNode::new(SomeNumber { value: 3 });
let d = InputNode::new(SomeNumber { value: 4 });
let e = InputNode::new(SomeNumber { value: 2 });

let a_times_b = DerivedNode::new(
    TwoNumbers::init(Rc::clone(&a), Rc::clone(&b)),
    Multiply,
    SomeNumber::default(),
);

let d_minus_c = DerivedNode::new(
    TwoNumbers::init(Rc::clone(&d), Rc::clone(&c)),
    Subtract,
    SomeNumber::default(),
);

...

Graphviz Deserialization

To simplify this process, there's the Graph derive macro. This takes a Graphviz definition of the graph, and generates the code to construct it.

// This is the graph generated by the previous example with slightly more
// descriptive node names.
#[derive(Graph)]
#[depends(
    digraph MyDag {
        a [label="SomeNumber"];
        b [label="SomeNumber"];
        c [label="SomeNumber"];
        d [label="SomeNumber"];
        e [label="SomeNumber"];
        a_times_b [label="SomeNumber"];
        a -> a_times_b [label="Multiply", class="TwoNumbersDep"];
        b -> a_times_b [label="Multiply", class="TwoNumbersDep"];
        d_minus_c [label="SomeNumber"];
        d -> d_minus_c [label="Subtract", class="TwoNumbersDep"];
        c -> d_minus_c [label="Subtract", class="TwoNumbersDep"];
        d_squared [label="SomeNumber"];
        d -> d_squared [label="Square"];
        e_squared [label="SomeNumber"];
        e -> e_squared [label="Square"];
        a_times_b_plus_c_minus_d [label="SomeNumber"];
        a_times_b -> a_times_b_plus_c_minus_d [label="Add", class="TwoNumbersDep"];
        d_minus_c -> a_times_b_plus_c_minus_d [label="Add", class="TwoNumbersDep"];
        times_e_squared [label="SomeNumber"];
        a_times_b_plus_c_minus_d -> times_e_squared [label="Multiply", class="TwoNumbersDep"];
        e_squared -> times_e_squared [label="Multiply", class="TwoNumbersDep"];
        minus_d_squared [label="SomeNumber"];
        times_e_squared -> minus_d_squared [label="Subtract", class="TwoNumbersDep"];
        d_squared -> minus_d_squared [label="Subtract", class="TwoNumbersDep"];
        cube_and_change_type [label="AnotherNumber"];
        minus_d_squared -> cube_and_change_type [label="Cube"];
    }
)]
pub struct DagCreator;

The macro generates the type MyDag<R>, similarly to the previous example. This graph can be created by passing the initial values for each of the nodes to the method attached to DagCreator.

// Provide initial values for all of the nodes.
let my_dag = DagCreator::create_my_dag(
    SomeNumber { value: 1 },
    SomeNumber { value: 2 },
    SomeNumber { value: 3 },
    SomeNumber { value: 4 },
    SomeNumber { value: 2 },
    SomeNumber::default(),
    SomeNumber::default(),
    SomeNumber::default(),
    SomeNumber::default(),
    SomeNumber::default(),
    SomeNumber::default(),
    SomeNumber::default(),
    AnotherNumber::default(),
);

let mut visitor = HashSetVisitor::new();

// The graph implements `Resolve`
{
    let output = my_dag.resolve_root(&mut visitor).unwrap();
    assert_eq!(output.value, -64);
}

// There are accessor methods to update inputs
my_dag.update_e(3).unwrap();

let output = my_dag.resolve_root(&mut visitor).unwrap();
assert_eq!(output.value, 1331);

Send-ability

Another benefit of using the Graph derive macro is that it will safely implement Send with unsafe for the given graph. This is a requirement for use in most async environments, such as Tokio.

Despite the fact that it uses Rc and RefCell internally, the Graph is safe to Send because it gives no access to the Rc types created, and moves all of them at once when being sent to another thread.

An example of this can be seen below:

// We can move the graph to other threads, despite the fact that it holds
// `Rc`s. This is safe because they are private, never cloned until dropped
// and always sent at the _same_ time.
let (my_dag, mut visitor) = std::thread::spawn(|| {
    // The graph provides a safe API for updating all input nodes.
    my_dag.update_c(10).unwrap();
    my_dag.update_b(6).unwrap();
    {
        let output = my_dag.resolve_root(&mut visitor).unwrap();
        assert_eq!(output.value, -4096);
    }
    (my_dag, visitor)
})
    .join()
    .unwrap();

my_dag.update_a(3).unwrap();

let output = my_dag.resolve_root(&mut visitor).unwrap();
assert_eq!(output.value, 778688);