Stage 1 — SeedPipeline
A SeedPipeline carries three base slots — a coalgebra plus an algebra:
#![allow(unused)]
fn main() {
/// Stage-1 typestate pipeline with three base slots: `grow`,
/// `seeds_from_node`, and `fold`. Used when the tree is discovered
/// lazily from `Seed` references.
#[must_use]
pub struct SeedPipeline<D, N, Seed, H, R>
where D: Domain<N>,
N: 'static, Seed: 'static, H: 'static, R: 'static,
{
pub(crate) grow: <D as Domain<N>>::Grow<Seed, N>,
pub(crate) seeds_from_node: <D as Domain<N>>::Graph<Seed>,
pub(crate) fold: <D as Domain<N>>::Fold<H, R>,
}
}
grow: Seed → N— resolves a reference (aSeed) into a full node (N).seeds_from_node: Edgy<N, Seed>— given a resolved node, enumerates the references it points to.fold: Fold<N, H, R>— the algebra over resolved nodes.
The pipeline operates lazily on demand: given an entry seed at run time,
it grows the tree by alternating grow and seeds_from_node until each
branch terminates at a leaf.
When to pick this over TreeishPipeline
Use SeedPipeline when the dependency graph speaks a different language
from the nodes — file paths, module names, URLs, anything that must be
resolved into a full data structure before its children can be examined.
When the nodes themselves already enumerate their children directly
(N → N*), TreeishPipeline is simpler: no grow slot.
Constructing one
#![allow(unused)]
fn main() {
#[test]
fn pipeline_overview_seed() {
use hylic_pipeline::prelude::*;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
struct Mod { cost: u64, deps: Vec<String> }
let reg: Arc<HashMap<String, Mod>> = Arc::new({
let mut m = HashMap::new();
m.insert("app".into(), Mod { cost: 1, deps: vec!["db".into()] });
m.insert("db".into(), Mod { cost: 2, deps: vec![] });
m
});
let reg_grow = reg.clone();
let sp: SeedPipeline<Shared, Mod, String, u64, u64> = SeedPipeline::new(
move |s: &String| reg_grow.get(s).cloned().unwrap(),
edgy_visit(|n: &Mod, cb: &mut dyn FnMut(&String)| {
for d in &n.deps { cb(d); }
}),
&fold(|n: &Mod| n.cost, |h: &mut u64, c: &u64| *h += c, |h: &u64| *h),
);
let r: u64 = sp
.filter_seeds(|s: &String| !s.starts_with('_'))
.run_from_slice(&FUSED, &["app".to_string()], 0u64);
// Reachable modules: app (cost 1) + db (cost 2) = 3.
assert_eq!(r, 3);
}
}
Stage-1 reshape
A SeedPipeline can be reshaped without lifting — the result is still a
SeedPipeline of (possibly different) type parameters. The
SeedSugarsShared
trait provides the surface; SeedSugarsLocal mirrors it for the Local
domain. Both come into scope via use hylic_pipeline::prelude::*;.
| method | changes |
|---|---|
filter_seeds(pred) | Seed set narrowed; types preserved |
wrap_grow(w) | intercepts every grow; types preserved |
map_node_bi(co, contra) | changes N to N2 via bijection |
map_seed_bi(to, from) | changes Seed to Seed2 via bijection |
Transitioning to Stage 2
Stage-2 sugars are not available on SeedPipeline directly — an
explicit .lift() is required. (TreeishPipeline auto-lifts; SeedPipeline
does not, because the Stage-2 chain operates over SeedNode<N> rather
than N, and an implicit transition would surface that asymmetry in
error messages.)
let lsp = pipeline
.lift() // → Stage2Pipeline<SeedPipeline<…>, IdentityLift>
.wrap_init(|n: &N, orig| orig(n) + 1)
.zipmap(|r: &R| classify(r)); // chain extends; tip R becomes (R, classification)
After .lift(), the chain operates on SeedNode<N> — but every Stage-2
sugar’s user closure types at &N. The SeedNode row is sealed and
auto-dispatched; see SeedNode<N> for the row’s shape
and the rare cases where it surfaces in a chain-tip type, and
Wrap dispatch for how the sugar trait reaches both
Bases through one body.
Running
Two equivalent surfaces:
- Direct on
SeedPipeline—.run(exec, entry_seeds, entry_heap)and.run_from_slice(exec, &[seeds], entry_heap)are inherent onSeedPipeline<D, …>itself. They forward throughself.clone().lift()internally; ergonomic shorthand for the common case where no Stage-2 sugars are chained. - On
Stage2Pipeline<SeedPipeline<…>, L>— same method names, same arguments. Used after.lift()plus any chain of Stage-2 sugars.
// Entry seeds as a slice (convenience), no sugars — direct on SeedPipeline:
let r: u64 = pipeline.run_from_slice(&FUSED, &["app".to_string()], 0u64);
// Entry seeds as a general Edgy<(), Seed>, no sugars:
let entry = edgy_visit(|_: &(), cb| cb(&"app".to_string()));
let r: u64 = pipeline.run(&FUSED, entry, 0u64);
// With Stage-2 sugars — `.lift()` is the explicit transition:
let r: u64 = pipeline
.lift()
.wrap_init(|n: &Mod, orig: &dyn Fn(&Mod) -> u64| orig(n) + 1)
.run_from_slice(&FUSED, &["app".to_string()], 0u64);
The last argument is the initial heap at the synthetic root level —
what the top-level accumulator starts with before any seed’s result is
folded in. It is always the base H type; the chain’s own MapH
is reached internally as the sugars promote from H outward.
.lift() itself is preserved as the explicit Stage-1 → Stage-2
transition. The shorthand on SeedPipeline exists to elide the
empty-.lift() ceremony at call sites that do not chain sugars; when
sugars are involved, .lift() makes the row-type transition (chain
input becoming SeedNode<N>) traceable to a single line in the
user’s source.
Full example
#![allow(unused)]
fn main() {
#[test]
fn seed_pipeline_example() {
use hylic_pipeline::prelude::*;
use std::collections::HashMap;
// The "registry" — flat data, not a tree.
let mut modules: HashMap<String, Vec<String>> = HashMap::new();
modules.insert("app".into(), vec!["db".into(), "auth".into()]);
modules.insert("db".into(), vec![]);
modules.insert("auth".into(), vec!["db".into()]);
// Edge function: given a module name, produce its dependency seeds.
let reg = modules.clone();
let seeds_from_node: Edgy<String, String> =
edgy_visit(move |name: &String, cb: &mut dyn FnMut(&String)| {
if let Some(deps) = reg.get(name) {
for dep in deps { cb(dep); }
}
});
// Fold: collect every reachable name.
let f: Fold<String, Vec<String>, Vec<String>> = fold(
|name: &String| vec![name.clone()],
|heap: &mut Vec<String>, child: &Vec<String>| heap.extend(child.iter().cloned()),
|heap: &Vec<String>| heap.clone(),
);
let pipeline: SeedPipeline<Shared, String, String, Vec<String>, Vec<String>> =
SeedPipeline::new(|seed: &String| seed.clone(), seeds_from_node, &f);
let result: Vec<String> = pipeline.run_from_slice(
&FUSED,
&["app".to_string()],
Vec::new(),
);
assert!(result.contains(&"app".to_string()));
assert!(result.contains(&"auth".to_string()));
}
}