Boxed Step
The boxed
step performs type erasure to reduce monomorphization and improve compile times. It wraps the current walker in a SmallBox
, breaking the chain of nested generic types that can grow exponentially in complex traversals.
The boxed walker preserves all functionality including context access and modification, making it a drop-in replacement for any walker step.
In this diagram:
- An Input Stream contains a walker with complex nested types like
Endpoints<Edges<Vertices<Empty>>>
. - The
.boxed()
step performs type erasure, wrapping the walker in aSmallBox
. - The Output Stream contains a simplified
BoxedVertexWalker
type that hides the complexity. - Type Complexity: Before boxing shows deeply nested generics; after boxing shows a simple erased type.
Syntax
walker.boxed()
Parameters
This step takes no parameters.
Return Value
Returns a new walker with erased types that behaves identically to the original but with simplified type signatures.
Examples
Basic Type Complexity Reduction
Reduce type complexity in multi-step traversals:
// Without boxing - creates complex nested types like:
// Endpoints<Edges<Endpoints<Edges<Vertices<Empty>>>>>
let _complex_type = graph
.walk()
.vertices(Vertex::person()) // Start with Person vertices
.edges_out() // Get outgoing edges
.head() // Move to target vertices
.edges_out() // Get their outgoing edges
.head(); // Move to final target vertices
// This creates deeply nested generic types that slow compilation
// With strategic boxing - simplified types
let people_connections: Vec<_> = graph
.walk()
.vertices(Vertex::person()) // Start with Person vertices
.edges_out() // Get outgoing edges
.boxed() // ← Breaks type complexity chain
.head() // Move to target vertices
.edges_out() // Get their outgoing edges
.boxed() // ← Further reduces complexity
.head() // Move to final target vertices
.collect();
println!("Found {} connected vertices through people", people_connections.len());
Strategic Boxing in Complex Traversals
Use boxing at logical checkpoints in long traversals:
// Complex multi-hop traversal with strategic boxing
let projects_via_people: Vec<_> = graph
.walk()
.vertices(Vertex::person()) // Start with Person vertices
.filter_by_person(|person, _| person.age() > 25) // Filter by age
.take(5) // Limit to first 5 people
.boxed() // ← Box after initial filtering
.edges_out() // Get their outgoing relationships
.head() // Move to connected vertices
.filter_project() // Keep only Project vertices
.boxed() // ← Box after project filtering
.collect();
println!("Found {} projects created by people over 25", projects_via_people.len());
Storage in Collections
Enable storage of walkers in data structures:
// Store different walker types in a collection using boxing
let vertex_walkers: Vec<Box<dyn Iterator<Item = _>>> = vec![
// First walker: all people
Box::new(
graph
.walk()
.vertices(Vertex::person()) // Type-safe Person lookup
.boxed() // ← Boxing makes storage possible
.into_iter()
),
// Second walker: young people only
Box::new(
graph
.walk()
.vertices(Vertex::person())
.filter_by_person(|person, _| person.age() < 30)
.boxed() // ← Boxing makes storage possible
.into_iter()
),
];
println!("Created {} different walker configurations", vertex_walkers.len());
Builder Pattern Integration
Use boxing to enable flexible walker construction:
// Builder pattern enabled by boxing
struct PersonTraversalBuilder<'graph, G: Graph> {
walker: Option<Box<dyn Iterator<Item = G::VertexId> + 'graph>>,
}
impl<'graph, G: Graph> PersonTraversalBuilder<'graph, G> {
fn add_young_people(&mut self, graph: &'graph G) {
let walker = graph
.walk()
.vertices(Vertex::person()) // Type-safe Person lookup
.filter_by_person(|person, _| person.age() < 30) // Filter young people
.boxed() // ← Boxing enables storage in the builder
.into_iter();
self.walker = Some(Box::new(walker));
}
}
let mut builder = PersonTraversalBuilder { walker: None };
builder.add_young_people(&graph);
Context Preservation
Boxing preserves context functionality - all context operations work normally:
// Context functionality is fully preserved through boxing
#[derive(Clone, Debug)]
struct TraversalMetrics {
hops: u32,
people_visited: u32,
}
let metrics_result: Vec<_> = graph
.walk()
.push_context(TraversalMetrics { hops: 0, people_visited: 0 })
.vertices(Vertex::person())
.mutate_context(|_vertex, ctx| {
ctx.people_visited += 1; // Track people visited
})
.edges_out()
.boxed() // ← Boxing preserves context functionality
.mutate_context(|_edge, ctx| {
ctx.hops += 1; // Context operations work normally after boxing
})
.head()
.map(|vertex, context| {
// Context is accessible in later steps
println!("Vertex {:?} reached after {} hops, visited {} people",
vertex, context.hops, context.people_visited);
vertex
})
.collect();
println!("Context-aware traversal found {} vertices", metrics_result.len());
Best Practices
- Use
boxed()
after 4+ chained operations to prevent type explosion - Place boxing at logical boundaries in complex traversals (after filtering, major operations)
- Apply when compile times become slow due to deep walker nesting
- Consider boxing when storing walkers in collections or data structures
- Avoid boxing simple 2-3 step chains where type complexity is manageable
- Measure the 5-15% runtime overhead against compilation benefits for your use case
Common Use Cases
- Compile-time optimization: Reducing build times for applications with many complex walker chains
- Type simplification: Making complex walker types manageable in library interfaces
- Collection storage: Enabling storage of different walker types in Vec or other collections
- Builder patterns: Simplifying type signatures in reusable walker construction utilities
- Library development: Providing clean APIs without exposing deeply nested generic types