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

Transformations

Features as standalone functions matching the transformation contract. One domain, one base fold, one base graph. Each feature is a named function — defined separately, plugged in with a single method call.

The phase-wrapping contract — each wrapper receives the original phase as a callable reference:

  • wrap_init: Fn(&N, &dyn Fn(&N) -> H) -> H
  • wrap_accumulate: Fn(&mut H, &R, &dyn Fn(&mut H, &R))
  • wrap_finalize: Fn(&H, &dyn Fn(&H) -> R) -> R
#![allow(unused)]
fn main() {
//! Transformations: features as standalone functions that match the contract.
//!
//! One domain, one base fold, one base graph. Each feature is a named
//! function — it IS the concern, separated and reusable. Plugging it
//! in is a single method call on the existing construct.

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::{Arc, Mutex};
    use hylic::prelude::*;
    use hylic::prelude::memoize_treeish_by;
    use insta::assert_snapshot;

    // ── Domain ──────────────────────────────────────────────

    #[derive(Clone, Debug)]
    struct Task {
        name: String,
        cost_ms: u64,
        deps: Vec<String>,
    }

    struct Registry(HashMap<String, Task>);

    impl Registry {
        fn new(tasks: &[(&str, u64, &[&str])]) -> Self {
            Registry(tasks.iter().map(|(name, cost, deps)| {
                (name.to_string(), Task {
                    name: name.to_string(),
                    cost_ms: *cost,
                    deps: deps.iter().map(|d| d.to_string()).collect(),
                })
            }).collect())
        }
        fn get(&self, name: &str) -> Option<&Task> { self.0.get(name) }
    }

    // ── Shared setup ────────────────────────────────────────

    fn setup() -> (Treeish<Task>, Task) {
        let reg = Registry::new(&[
            ("app",       50,  &["compile", "link"]),
            ("compile",   200, &["parse", "typecheck"]),
            ("parse",     100, &[]),
            ("typecheck", 300, &[]),
            ("link",      150, &[]),
        ]);
        let map = reg.0.clone();
        let g: Treeish<Task> = treeish(move |task: &Task| {
            task.deps.iter().filter_map(|d| map.get(d).cloned()).collect()
        });
        let root = reg.get("app").unwrap().clone();
        (g, root)
    }

    fn base_fold() -> Fold<Task, u64, u64> {
        fold(
            |t: &Task| t.cost_ms,
            |heap: &mut u64, child: &u64| *heap += child,
            |h: &u64| *h,
        )
    }

    // ── Fold phase wrappers ─────────────────────────────────
    //
    // Each is a standalone closure matching the wrap contract:
    //   wrap_init:       Fn(&N, &dyn Fn(&N) -> H) -> H
    //   wrap_accumulate: Fn(&mut H, &R, &dyn Fn(&mut H, &R))
    //   wrap_finalize:   Fn(&H, &dyn Fn(&H) -> R) -> R

    /// Hooks into init: called once per node, before children.
    /// Logs the task name, then delegates to the original init.
    fn visit_logger(sink: Arc<Mutex<Vec<String>>>)
        -> impl Fn(&Task, &dyn Fn(&Task) -> u64) -> u64
    {
        move |task: &Task, orig: &dyn Fn(&Task) -> u64| {
            sink.lock().unwrap().push(task.name.clone());
            orig(task)
        }
    }

    /// Hooks into accumulate: conditionally skips small children.
    /// By not calling orig, the child result is never folded in.
    fn skip_small_children(threshold: u64)
        -> impl Fn(&mut u64, &u64, &dyn Fn(&mut u64, &u64))
    {
        move |heap: &mut u64, child: &u64, orig: &dyn Fn(&mut u64, &u64)| {
            if *child >= threshold { orig(heap, child); }
        }
    }

    /// Hooks into finalize: clamps the result.
    fn clamp_at(max: u64)
        -> impl Fn(&u64, &dyn Fn(&u64) -> u64) -> u64
    {
        move |heap: &u64, orig: &dyn Fn(&u64) -> u64| orig(heap).min(max)
    }

    /// zipmap contract: a plain Fn(&R) -> Extra. No wrapping needed —
    /// the function itself IS the feature. zipmap calls it per node,
    /// pairing the original result with the derived value: R → (R, Extra).
    fn classify(total: &u64) -> &'static str {
        match *total {
            t if t >= 500 => "critical",
            t if t >= 200 => "heavy",
            _ => "light",
        }
    }

    // ── Graph transformations ───────────────────────────────

    fn only_costly_deps(g: &Treeish<Task>, min_cost: u64) -> Treeish<Task> {
        let inner = g.clone();
        treeish(move |task: &Task| {
            inner.at(task)
                .filter(|child: &Task| child.cost_ms >= min_cost)
                .collect_vec()
        })
    }

    // ── Tests ───────────────────────────────────────────────

    #[test]
    fn test_visit_logger() {
        let (graph, root) = setup();
        let visited = Arc::new(Mutex::new(Vec::new()));
        let fold = base_fold().wrap_init(visit_logger(visited.clone()));

        let total = FUSED.run(&fold, &graph, &root);
        let names: Vec<String> = visited.lock().unwrap().clone();
        assert_eq!(total, 800);
        assert_snapshot!("visit_logger", format!(
            "total={total}, visited: {}", names.join(" → ")
        ));
    }

    #[test]
    fn test_skip_small_children() {
        let (graph, root) = setup();
        let fold = base_fold().wrap_accumulate(skip_small_children(200));
        let total = FUSED.run(&fold, &graph, &root);
        // app(50) + compile(200+typecheck 300) = 550; parse(100) and link(150) skipped
        assert_eq!(total, 550);
        assert_snapshot!("skip_small", format!("total={total} (small children skipped)"));
    }

    #[test]
    fn test_clamp_at() {
        let (graph, root) = setup();
        let fold = base_fold().wrap_finalize(clamp_at(500));
        let total = FUSED.run(&fold, &graph, &root);
        // compile=min(600,500)=500, link=150, app=min(50+500+150,500)=500
        assert_eq!(total, 500);
        assert_snapshot!("clamp_at", format!("total={total} (clamped at 500)"));
    }

    #[test]
    fn test_classify() {
        let (graph, root) = setup();
        let (total, category) = FUSED.run(&base_fold().zipmap(classify), &graph, &root);
        assert_eq!(total, 800);
        assert_eq!(category, "critical");
        assert_snapshot!("classify", format!("total={total}, category={category}"));
    }

    #[test]
    fn test_only_costly_deps() {
        let (graph, root) = setup();
        let filtered = only_costly_deps(&graph, 150);
        let total = FUSED.run(&base_fold(), &filtered, &root);
        // parse(100) pruned: app(50)+compile(200)+typecheck(300)+link(150) = 700
        assert_eq!(total, 700);
        assert_snapshot!("only_costly", format!("total={total} (deps with cost < 150 pruned)"));
    }

    #[test]
    fn test_memoize_diamond() {
        let reg = Registry::new(&[
            ("app", 10, &["compile", "link"]),
            ("compile", 50, &["stdlib"]),
            ("link", 30, &["stdlib"]),
            ("stdlib", 200, &[]),
        ]);
        let visit_count = Arc::new(Mutex::new(0u32));
        let vc = visit_count.clone();
        let map = reg.0.clone();
        let graph = treeish(move |task: &Task| {
            *vc.lock().unwrap() += 1;
            task.deps.iter().filter_map(|d| map.get(d).cloned()).collect()
        });
        let root = reg.get("app").unwrap().clone();

        let total = FUSED.run(&base_fold(), &graph, &root);
        let raw_visits = *visit_count.lock().unwrap();

        *visit_count.lock().unwrap() = 0;
        let cached = memoize_treeish_by(&graph, |t: &Task| t.name.clone());
        let total_memo = FUSED.run(&base_fold(), &cached, &root);
        let memo_visits = *visit_count.lock().unwrap();

        assert_eq!((total, raw_visits), (490, 5));
        assert_eq!((total_memo, memo_visits), (490, 4));
        assert_snapshot!("memoize", format!(
            "raw: total={total} visits={raw_visits}, memo: total={total_memo} visits={memo_visits}"
        ));
    }

    #[test]
    fn test_composed_pipeline() {
        let (graph, root) = setup();
        let visited = Arc::new(Mutex::new(Vec::new()));
        let pipeline = base_fold()
            .wrap_init(visit_logger(visited.clone()))
            .wrap_finalize(clamp_at(500))
            .zipmap(classify);

        let (total, category) = FUSED.run(&pipeline, &graph, &root);
        let names: Vec<String> = visited.lock().unwrap().clone();
        assert_eq!(total, 500);
        assert_eq!(category, "critical");
        assert_snapshot!("composed", format!(
            "total={total} [{category}], visited: {}", names.join(" → ")
        ));
    }
}
}

Outputs:

total=800, visited: app → compile → parse → typecheck → link
total=550 (small children skipped)
total=500 (clamped at 500)
total=800, category=critical
total=700 (deps with cost < 150 pruned)
raw: total=490 visits=5, memo: total=490 visits=4
total=500 [critical], visited: app → compile → parse → typecheck → link