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

Graph: controlling traversal

The graph — Treeish<N>, or the more general Edgy<N, E> — is a function from a node to its children, and determines what is visited during the fold. The node type N may be any type: a struct, an integer index, a string key, a database identifier. The structure of the tree resides in the function, not in the data.

Constructors

Three means of creating a Treeish<N>:

#![allow(unused)]
fn main() {
    #[test]
    fn treeish_constructors() {
        use hylic::prelude::*;

        #[derive(Clone)]
        struct Node { value: u64, children: Vec<Node> }

        let root = Node { value: 1, children: vec![Node { value: 2, children: vec![] }] };

        // Callback-based (zero allocation per visit):
        let g1: Treeish<Node> = treeish_visit(|n: &Node, cb: &mut dyn FnMut(&Node)| {
            for child in &n.children { cb(child); }
        });

        // Vec-returning (allocates per visit):
        let g2: Treeish<Node> = treeish(|n: &Node| n.children.clone());

        // Slice accessor (borrows, zero allocation):
        let g3: Treeish<Node> = treeish_from(|n: &Node| n.children.as_slice());

        assert_eq!(g1.apply(&root).len(), 1);
        assert_eq!(g2.apply(&root).len(), 1);
        assert_eq!(g3.apply(&root).len(), 1);

        // Flat data — nodes are indices, children from adjacency list:
        let adj: Vec<Vec<usize>> = vec![vec![1, 2], vec![], vec![]];
        let g4: Treeish<usize> = treeish_visit(move |n: &usize, cb: &mut dyn FnMut(&usize)| {
            for &c in &adj[*n] { cb(&c); }
        });
        assert_eq!(g4.apply(&0).len(), 2);
    }
}

treeish_visit is the most general form; its callback receives each child without the allocation of a Vec. treeish wraps a Vec-returning function for convenience, and treeish_from extracts a slice reference from a field.

For non-nested data — adjacency lists, maps, external lookups — treeish_visit is the appropriate constructor:

// Adjacency list: nodes are indices
let adj: Vec<Vec<usize>> = vec![vec![1, 2], vec![3], vec![], vec![]];
let graph = treeish_visit(move |n: &usize, cb: &mut dyn FnMut(&usize)| {
    for &c in &adj[*n] { cb(&c); }
});

// HashMap-backed graph: nodes are string keys
let edges: HashMap<String, Vec<String>> = /* ... */;
let graph = treeish_visit(move |n: &String, cb: &mut dyn FnMut(&String)| {
    if let Some(children) = edges.get(n) {
        for c in children { cb(c); }
    }
});

For a runnable adjacency-list example, see intro_flat_example.

Edge transformations

The Edgy<N, E> type generalises Treeish<N> by allowing edges and nodes to be different types. Combinators transform the edge type or the node type:

%3E1Edgy<N, E>E2Edgy<N, E2>map(f)E1->E2transform edgesE3Edgy<N2, E>contramap(f)E1->E3change node typeE4Edgy<N, E>filter(pred)E1->E4prune edges

filter — pruning children

#![allow(unused)]
fn main() {
    #[test]
    fn graph_filter() {
        use hylic::prelude::*;

        #[derive(Clone)]
        struct Node { value: u64, children: Vec<Node> }

        let graph: Treeish<Node> = treeish(|n: &Node| n.children.clone());
        let f:     Fold<Node, u64, u64> = fold(
            |n: &Node| n.value,
            |h: &mut u64, c: &u64| *h += c,
            |h: &u64| *h,
        );

        let root = Node { value: 1, children: vec![
            Node { value: 10, children: vec![] },
            Node { value: 2, children: vec![] },
        ]};

        // Only visit children with value > 5.
        let pruned: Treeish<Node> = graph.filter(|child: &Node| child.value > 5);
        let result: u64 = FUSED.run(&f, &pruned, &root);
        assert_eq!(result, 11); // 1 + 10 (skipped 2)
    }
}

The fold sees fewer children without any awareness that pruning has occurred.

Caching: memoize_treeish

For DAGs in which the same node is reachable from multiple parents, memoize_treeish caches the child enumeration:

#![allow(unused)]
fn main() {
    #[test]
    fn memoize_example() {
        use hylic::prelude::*;
        use std::sync::atomic::{AtomicUsize, Ordering};
        use std::sync::Arc;

        let call_count = Arc::new(AtomicUsize::new(0));
        let cc = call_count.clone();

        let graph: Treeish<u64> = treeish(move |n: &u64| -> Vec<u64> {
            cc.fetch_add(1, Ordering::Relaxed);
            if *n == 0 { vec![] } else { vec![n - 1] }
        });
        let f: Fold<u64, u64, u64> = fold(
            |n: &u64| *n,
            |h: &mut u64, c: &u64| *h += c,
            |h: &u64| *h,
        );

        let cached: Treeish<u64> = memoize_treeish(&graph);
        let _ = FUSED.run(&f, &cached, &3u64);
        let first_count = call_count.load(Ordering::Relaxed);

        // Second run hits the cache; no new calls into `graph`.
        let _ = FUSED.run(&f, &cached, &3u64);
        let second_count = call_count.load(Ordering::Relaxed);
        assert_eq!(first_count, second_count);
    }
}

The first visit to a key computes and caches its children; subsequent visits return the cached result.

Visit combinator

Edgy::at(node) returns a Visit<T, F> — a push-based iterator exposing map, filter, fold, count, and collect_vec. All combinators are callback-based internally.