[][src]Crate rgraph

The rGaph crate:

This library provides the mechanisms to define a directed acyclic graph of tasks. Once the graph is generated, a solver object can be instantiated to execute any of the tasks defined.

High Level description

Tasks are defined in the terms of:

  • Its input value
  • Its output values
  • A procedure body that can carry out a task

The values used as inputs and outputs by the system are named assets. Assets can be:

  • Input Assets: for values fed into a task.
  • Output Assets: for values produced by a task.
  • Freestanding Assets: constant values fed into the system which are not computed by any task.

With items we can construct a graph of task and execute it in the following manner:

  1. Create a set of tasks, each one with its own input and outputs.
  2. Define the order of stages of the computation graph by attaching outputs into the next task inputs, this is called binding. It is not required that all assets are bound, but it is required that all assets are bound for each task transitivelly involved in a path throw the graph. This, for example, can be used to add debug tasks that can be dynamically activated and lazily evaluated.
  3. Initialize a cache to store the assets during graph computation, this can be used afterwards to retrieve the values.
  4. Solve the graph: there are currently two methods to solve a graph:
    • execute: where the parameter is the name of the task we want to execute. Prerequisites will be identified and executed, if not possible because the topology is ill formed, an error will be returned.
    • execute_terminals: terminal tasks are those with no outputs. Any number of terminal tasks can be defined, all of them will be executed if prerequistes can be satisfied, otherwise an error will be returned.

Use by example

In order to satisfy the input of such task, all the producer tasks will be executed as well.

A task can be defined like you would define a function, it requires:

  • A name
  • A list of inputs, that well may be empty.
  • A list of outputs, which can be empty as well.
  • Body, executing the code necessary to produce the outputs out of the inputs.

The macro create_node! will help you out with this task:

use rgraph::*;

create_node!(
         task_name  (a: u32, b : u32) -> (output: u32) {
             // return is done by assigning to the output variable
             output = a + b;
         }
     );

The body of the task will be executed by a move lambda, this enforces some guarantees. Nevertheless if the tasks need to execute some side effects, you may keep in mind that:

  • Objects need to be cloned into the task scope.
  • Only runtime borrowing can be checked at this point.
  • The Solver has no knowledge of data changes done via global access. It only tracks assets registered as inputs or outputs of the task. For this reason tasks may not be executed a second time as long as the inputs do not change. This may turn into side effects not happening because the requirements were not declared correctly.

Once the tasks are defined, you can bind the input assets to the output produced by other task or feed directly into the Solver.

use rgraph::*;
let mut g = Graph::new();
  
g.add_node(create_node!(
         task1  () -> (out_asset: u32) {
             // .... task body
             out_asset = 1;
         }
     ));
      
g.add_node(create_node!(
         task2  (in_asset : u32) -> () {
             // .... task body
         }
     ));
  
g.bind_asset("task1::out_asset", "task2::in_asset").expect(" tasks and assets must exist");

Finally, to execute the Graph:

  • Create an assets cache object (which can be reused to execute the graph again)
  • Create a solver, to be used one single time and then dropped.
use rgraph::*;
let mut g = Graph::new();
  
 // ... create graph and bind the assets

let mut cache = ValuesCache::new();
let mut solver = GraphSolver::new(&g, &mut cache);
// terminal tasks are those which do not produce output
// the following line will traverse the graph and execute all tasks needed
// to satisfy the terminal tasks.
solver.execute_terminals().unwrap();

Modules

printer

Macros

asset_str
asset_string
create_node

Macro to generate a Node (Task). It requires: a name (as used in the solver to execute it), a set of inputs, a set of outputs, and a set of statements which are the body of the task

Structs

Graph

The graph class itself. It holds the static information about the tasks (Nodes) and how they depend on each other by waiting on resources (Assets)

GraphSolver

The graph solver is a transient object which can execute the tasks described in a graph. It is designed to be generated and dropped on every execution.

Node

Generic that stores the information required to execute arbitrary tasks Please use create_node macro to instantiate this objects

Enums

AssetProvider

Replacement for Option since input assets may be satisfied by a freestanding asset as well.

GraphError

Errors that may happen during Graph construction

SolverError

Errors that may happen during a Solver instance execution

SolverStatus

Type to differentiate cached tasks from executed ones

Traits

Cache

A convenience trait to allow the storage of asset values in between tasks or graph executions.

Comparable

this trait allows us to overload behavior for custom types in this manner comparison can be optimized or bypassed for custom types

NodeRunner

helper trait that hides heterogeneous tasks behind a common interface

Type Definitions

ValuesCache

type used to store results of executions and pass it to further solver instances