Parallel execution
hylic provides two built-in executors. FUSED runs the fold
sequentially through callback-based recursion. The Funnel executor
parallelizes the same fold across a scoped thread pool. Both are
invoked through the same .run() method.
Sequential: FUSED
Callback-based recursion on a single thread, with no overhead beyond the fold closures themselves:
#![allow(unused)]
fn main() {
use hylic::prelude::*;
FUSED.run(&fold, &graph, &root);
}
Parallel: Funnel
The Funnel executor preserves the fused property — children are
discovered through graph.visit and processed concurrently. No
intermediate tree is built.
One-shot
#![allow(unused)]
fn main() {
use hylic::prelude::*;
exec(funnel::Spec::default(8)).run(&fold, &graph, &root);
}
Spec::default(n) uses the Robust policy preset. .run() creates
a scoped thread pool internally, runs the fold, and joins before
returning.
Session scope
For repeated folds, amortize pool creation:
#![allow(unused)]
fn main() {
exec(funnel::Spec::default(8)).session(|s| {
s.run(&fold1, &graph1, &root1);
s.run(&fold2, &graph2, &root2);
});
}
The pool lives for the closure. Each .run() inside is cheap.
Explicit attach
Provide the pool yourself:
#![allow(unused)]
fn main() {
funnel::Pool::with(8, |pool| {
let pw = exec(funnel::Spec::default(8)).attach(pool);
let sh = exec(funnel::Spec::for_wide_light(8)).attach(pool);
pw.run(&fold, &graph, &root);
sh.run(&fold, &graph, &root);
});
}
Different policies can share a pool — each .attach() consumes a
(Copy) Spec and binds it to the pool, producing a session-level
executor.
Policy variants
| Preset | Best for |
|---|---|
Spec::default(n) | General purpose |
Spec::for_wide_light(n) | Wide trees (bf > 10) |
Spec::for_deep_narrow(n) | Deep chains (bf = 2) |
Spec::for_low_overhead(n) | Overhead-sensitive |
Spec::for_high_throughput(n) | Heavy balanced |
See Funnel policies for the full decision
guide and The Exec pattern for
the type-level design behind .run(), .session(), and .attach().
External parallel options
One additional strategy lives in a sibling crate:
- Rayon (hylic-benchmark):
par_iter-based fork-join
Working example
This example uses a flat adjacency list — nodes are integer indices, children are looked up by index. The same fold runs sequentially (Fused) and in parallel (Funnel) with identical results.
#![allow(unused)]
fn main() {
//! Parallel execution: Fused vs Funnel over flat data.
//! Demonstrates: adjacency-list graph, identical results across
//! policies, session scope, explicit pool attach.
#[cfg(test)]
mod tests {
use hylic::prelude::*;
use hylic::exec::funnel;
use insta::assert_snapshot;
/// Build a tree as a flat adjacency list + value array.
/// Node 0 is the root with 6 children; each child has 3 leaves.
fn build_tree() -> (Vec<Vec<usize>>, Vec<u64>) {
let mut adj: Vec<Vec<usize>> = Vec::new();
let mut vals: Vec<u64> = Vec::new();
// root (node 0)
adj.push((1..=6).collect());
vals.push(1);
// 6 branches (nodes 1-6), each with 3 leaves
let mut next_leaf = 7;
for i in 0..6 {
let children: Vec<usize> = (next_leaf..next_leaf + 3).collect();
adj.push(children);
vals.push(i as u64 * 10);
next_leaf += 3;
}
// 18 leaves (nodes 7-24)
for i in 0..6 {
for j in 0..3u64 {
adj.push(vec![]);
vals.push(i as u64 * 10 + j);
}
}
(adj, vals)
}
#[test]
fn parallel_strategies() {
let (adj, vals) = build_tree();
// The treeish looks up children by index; no nested structs.
let adj_for_graph = adj.clone();
let graph: Treeish<usize> = treeish_visit(move |n: &usize, cb: &mut dyn FnMut(&usize)| {
for &c in &adj_for_graph[*n] { cb(&c); }
});
let vals_for_fold = vals.clone();
let sum: Fold<usize, u64, u64> = fold(
move |n: &usize| vals_for_fold[*n],
|heap: &mut u64, child: &u64| *heap += child,
|heap: &u64| *heap,
);
// Sequential baseline
let expected = FUSED.run(&sum, &graph, &0usize);
// One-shot: .run() creates + destroys pool internally
let r_default = exec(funnel::Spec::default(4)).run(&sum, &graph, &0usize);
assert_eq!(r_default, expected);
// Different policy: wide-light
let r_wide = exec(funnel::Spec::for_wide_light(4)).run(&sum, &graph, &0usize);
assert_eq!(r_wide, expected);
// Session scope: pool shared across folds
exec(funnel::Spec::default(4)).session(|s| {
assert_eq!(s.run(&sum, &graph, &0usize), expected);
assert_eq!(s.run(&sum, &graph, &0usize), expected);
});
// Explicit attach: manual pool, multiple policies
funnel::Pool::with(4, |pool| {
let pw = exec(funnel::Spec::default(4)).attach(pool);
let sh = exec(funnel::Spec::for_wide_light(4)).attach(pool);
assert_eq!(pw.run(&sum, &graph, &0usize), expected);
assert_eq!(sh.run(&sum, &graph, &0usize), expected);
});
assert_snapshot!("parallel", format!(
"sum = {expected}, verified: fused, funnel(one-shot), funnel(wide), session, attach"
));
}
}
}
Output:
sum = 619, verified: fused, funnel(one-shot), funnel(wide), session, attach