Context System
The context system is a powerful feature of the Graph API that enriches your traversals by carrying data alongside the stream of elements (vertices or edges). This allows you to maintain state, collect information, and make decisions based on previously processed elements as you explore the graph.
What is Context?
In this diagram:
- An Input Stream contains elements A and B.
- The
push_context
step is applied. It calculates a context value (e.g.,"Ctx_A"
) based on the first element encountered (A) and attaches this fixed context to all subsequent elements. - The Stream with Context now contains pairs of
(Element, Context)
:(A, "Ctx_A")
,(B, "Ctx_A")
. - A subsequent
map
step receives both the current element and its associated context, allowing it to perform transformations using both pieces of information. - The Final Output shows the results produced by the
map
step.
Context allows you to:
- Carry information from previous steps to later steps in the traversal
- Transform data as you move through the graph
- Make decisions based on a combination of current and past elements
- Build complex data structures by accumulating information during traversal
- Maintain state without changing the traversal position
Context Methods
The Graph API provides two primary methods for managing context:
push_context
// Add or transform context
walker.push_context(|element, current_context| {
// Extract data from the current element
// Optionally use the existing context
// Return a new context value
})
This method creates or transforms context based on the current element and any existing context.
push_default_context
// Add the current vertex or edge as context
walker.push_default_context()
This method stores the current vertex or edge in context, making it available in later steps.
Basic Context Usage
Here's a simple example of storing information in context and using it later:
pub fn basic_context_example() {
let graph = standard_populated_graph();
// Store person name in context and use it later
let person_projects = graph
.walk()
.vertices()
.filter_person() // Type-safe filter using generated helper
.push_context(|v, _| {
// Extract and store person's name
v.project::<Person<_>>().unwrap().name().to_string()
})
.edges(EdgeSearch::scan().outgoing())
.filter_created() // Type-safe filter using generated helper
.vertices()
.filter_project() // Type-safe filter using generated helper
.map(|project, ctx| {
// Format using project name and person name from context
format!(
"{} created the {} project",
ctx,
project.project::<Project<_>>().unwrap().name()
)
})
.collect::<Vec<_>>();
}
Nested Context with Detours
When using the detour
step, context becomes nested, allowing you to access the parent context:
pub fn nested_context_example() {
let graph = standard_populated_graph();
// Use nested context to track relationships
let follows_ages = graph
.walk()
.vertices()
.filter_person() // Type-safe filter using generated helper
.push_context(|v, _| {
// Store source person's age in context
v.project::<Person<_>>().unwrap().age()
})
.detour(|w| {
// Start a sub-traversal that keeps parent context
w.edges(EdgeSearch::scan().outgoing())
.filter_follows() // Type-safe filter using generated helper
.vertices()
.filter_person() // Type-safe filter using generated helper
.map(|target, ctx| {
// Access source's age from context
let source_age = *ctx;
let target_age = target.project::<Person<_>>().unwrap().age();
// Calculate and return age difference
let diff = target_age as i32 - source_age as i32;
if diff > 0 {
"follows someone older"
} else if diff < 0 {
"follows someone younger"
} else {
"follows someone the same age"
}
})
})
.collect::<Vec<_>>();
}
Default Context
The Graph API provides a built-in default context that simplifies common patterns:
pub fn default_context_example() {
let graph = standard_populated_graph();
// Use the built-in default context to access source vertex
let relationships = graph
.walk()
.vertices()
.filter_person() // Type-safe filter using generated helper
.push_default_context() // Store current vertex in default context
.edges(EdgeSearch::scan().outgoing())
.vertices()
.map(|target, ctx| {
// Access the source vertex from context
let source = ctx.vertex();
let source_name = source
.project::<Person<_>>()
.map(|p| p.name())
.unwrap_or("Unknown");
// Format the relationship based on target type
match target.weight() {
Vertex::Person { name, .. } => format!("{} is connected to {}", source_name, name),
Vertex::Project { name, .. } => format!("{} works on {}", source_name, name),
_ => "Unknown relationship".to_string(),
}
})
.collect::<Vec<_>>();
}
Type Safety
The context system is fully type-safe:
- Each context value has a specific type determined by your context function
- The compiler enforces correct handling of context types
- Context functions must return values compatible with downstream operations
- Errors in context type handling are caught at compile time, not runtime
Context Lifecycle
- Creation: Context is created when you first call a context method
- Transformation: Context can be transformed at any step in the traversal
- Access: Any step that accepts a closure can access the context
- Nesting: Detours create nested contexts with access to parent contexts
- Immutability: Context is immutable; transformations create new contexts
Best Practices
- Keep context small: Large contexts can impact performance
- Use immutable data structures: Create new contexts rather than modifying existing ones
- Leverage type safety: Let the compiler ensure your context manipulations are valid
- Consider cloning costs: For large data, use
Arc
or similar for cheap cloning - Use default context: For simple cases,
push_default_context()
is cleaner than custom context - Chain context operations: Build complex data structures through multiple context steps
- Document context types: When using complex context chains, document the context type at each step
Common Use Cases
- Path tracking: Record the path taken through the graph
- Property collection: Gather properties from different vertices/edges
- Decision making: Use information from earlier elements to influence traversal decisions
- Aggregation: Build composite results during traversal
- Statistical analysis: Calculate metrics as you traverse the graph