Case study — Explainer
explainer_lift is a ShapeLift constructor that wraps a fold
with per-node trace recording. It’s a useful case study because
it changes H and R (not N), composes as a post-lift, and
produces a result type that lets callers inspect the full
computation tree.
What it does
#![allow(unused)]
fn main() {
pub fn explainer_lift<N, H, R>()
-> ShapeLift<Shared, N, H, R,
N,
ExplainerHeap<N, H, ExplainerResult<N, H, R>>,
ExplainerResult<N, H, R>>
where N: Clone + Send + Sync + 'static,
H: Clone + Send + Sync + 'static,
R: Clone + Send + Sync + 'static,
{
let fold_xform: <Shared as ShapeCapable<N>>::FoldXform<
H, R, N,
ExplainerHeap<N, H, ExplainerResult<N, H, R>>,
ExplainerResult<N, H, R>,
> = Arc::new(move |f: Fold<N, H, R>| {
let f1 = f.clone();
let f2 = f.clone();
let f3 = f;
sfold::fold(
move |n: &N| ExplainerHeap::new(n.clone(), f1.init(n)),
move |heap: &mut ExplainerHeap<N, H, ExplainerResult<N, H, R>>,
child: &ExplainerResult<N, H, R>| {
f2.accumulate(&mut heap.working_heap, &child.orig_result);
heap.transitions.push(ExplainerStep {
incoming_result: child.clone(),
resulting_heap: heap.working_heap.clone(),
});
},
move |heap: &ExplainerHeap<N, H, ExplainerResult<N, H, R>>| ExplainerResult {
orig_result: f3.finalize(&heap.working_heap),
heap: heap.clone(),
},
)
});
ShapeLift::new(
<Shared as ShapeCapable<N>>::identity_treeish_xform(),
fold_xform,
)
}
}
The lift wraps:
- H becomes
ExplainerHeap<N, H, ExplainerResult<N, H, R>>: the original H plus a vector of per-child transitions recorded during accumulate. - R becomes
ExplainerResult<N, H, R>: the original result plus the full heap (so callers can walk the trace tree).
Every node’s finalize produces both the original R and the recorded history.
Usage
Via the sugar method .explain() on any Stage-2 pipeline — a
treeish-rooted Stage2Pipeline, a seed-rooted Stage2Pipeline,
or a TreeishPipeline via auto-lift. A SeedPipeline requires
an explicit .lift() first:
#![allow(unused)]
fn main() {
#[test]
fn explainer_usage() {
use hylic_pipeline::prelude::*;
#[derive(Clone)]
struct N { val: u64, children: Vec<N> }
let f: Fold<N, u64, u64> = fold(
|n: &N| n.val,
|h: &mut u64, c: &u64| *h += c,
|h: &u64| *h,
);
let root = N { val: 1, children: vec![N { val: 2, children: vec![] }] };
let trace: ExplainerResult<N, u64, u64> =
TreeishPipeline::new(treeish(|n: &N| n.children.clone()), &f)
.lift()
.then_lift(Shared::explainer_lift::<N, u64, u64>())
.run_from_node(&FUSED, &root);
assert_eq!(trace.orig_result, 3);
}
}
The return type is ExplainerResult<N', H, R> where N' is the
chain’s current node type — N on a treeish-rooted chain, but
SeedNode<N> on a seed-rooted chain (since the seed chain’s
node type is SeedNode<N> from .lift() onward). Access
.orig_result for the original computation’s output:
#![allow(unused)]
fn main() {
#[test]
fn explainer_orig_result() {
use hylic_pipeline::prelude::*;
#[derive(Clone)]
struct Node { v: u64, ch: Vec<Node> }
let root = Node { v: 3, ch: vec![
Node { v: 2, ch: vec![] },
Node { v: 1, ch: vec![] },
]};
let tp: TreeishPipeline<Shared, Node, u64, u64> = TreeishPipeline::new(
treeish(|n: &Node| n.ch.clone()),
&fold(|n: &Node| n.v, |h: &mut u64, c: &u64| *h += c, |h: &u64| *h),
);
let trace: ExplainerResult<Node, u64, u64> = tp
.explain()
.run_from_node(&FUSED, &root);
// Sum = 3 + 2 + 1 = 6.
assert_eq!(trace.orig_result, 6);
// Every non-leaf records its child-accumulations.
assert!(!trace.heap.transitions.is_empty());
}
}
Sealed view on the seed path
For an N-typed view of the trace that hides SeedNode entirely,
project via the standard From conversion:
use hylic::prelude::SeedExplainerResult;
let raw: ExplainerResult<SeedNode<N>, H, R> =
pipeline.lift().explain().run_from_slice(&FUSED, &seeds, h0);
let sealed: SeedExplainerResult<N, H, R> = raw.into();
// sealed.entry_initial_heap, entry_working_heap, orig_result — EntryRoot row promoted out
// sealed.roots: Vec<ExplainerResult<N, H, R>> — per-seed subtrees
Use raw when you need to keep composing lifts on top of
.explain() (the chain type is what matters); use sealed when
you want an N-typed view for formatting or assertions — the
library’s invariant guarantees every below-root node is a
Node(n), so the unwrap is total.
Composing with other lifts
Because explain() is just a then_lift(Shared::explainer_lift()),
it composes:
let r = pipeline
.wrap_init(|n, orig| orig(n) * 2) // first lift
.explain() // records the wrap_init results
.zipmap(|r| r.orig_result > 100); // inspect .orig_result
Order matters: lifts run bottom-up (the first .wrap_init runs
innermost; .explain sees its results; .zipmap sees the
ExplainerResult).
Streaming variant
Shared::explainer_describe_lift(fmt, emit) emits formatted
trace lines per node via a callback and leaves MapR = R
unchanged:
use hylic::prelude::*;
let _ = Shared::explainer_describe_lift::<Node, u64, u64, _, _>(
trace_fold_compact::<Node, u64, u64>,
|line: &str| eprintln!("[trace] {line}"),
);
Local mirror deferred (blocked on Send+Sync in the formatter);
explainer_lift is available for Local.