Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 (a Seed) 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.

%3topentry_seeds: ["app"]s0seed: "app"top->s0entry seedsn0node: Appdeps: ["db", "auth"]s0->n0grows1seed: "db"n0->s1seeds_from_nodes2seed: "auth"n0->s2seeds_from_noden1node: Dbdeps: []s1->n1grown2node: Authdeps: ["db"]s2->n2grows3seed: "db"n2->s3seeds_from_noden3node: Dbdeps: []s3->n3grow

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::*;.

methodchanges
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 on SeedPipeline<D, …> itself. They forward through self.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()));
    }
}