Writing a custom Lift
Most transformations compose out of the library catalogue and
the sugar traits. A custom Lift impl is the right tool when
the transformation carries cross-node state, requires
per-variant dispatch on the input N, or is itself an execution
strategy.
apply has one job: produce the three output slots and hand
them to a continuation cont(grow', treeish', fold'). Everything
about the impl follows from four decisions about how those
slots relate to the input ones.
Four decisions
1. Output types.
type N2 = ???;
type MapH = ???;
type MapR = ???;
Mirror the input on axes the lift does not change. Where an
axis changes, declare the new type. MapH and MapR are
typically wrappers — the explainer, for instance, wraps MapR
into ExplainerResult<N, H, R>.
2. Treatment of the input grow.
Three options: pass through unchanged, wrap with an N-conversion
when N changes, or synthesise a fresh grow (the SeedLift
case, where the chain head closes the grow axis). Most custom
lifts pass through.
3. Treatment of the input treeish.
Pass through, filter, wrap in a visit-intercepting closure, or rebuild entirely.
4. Treatment of the input fold.
Clone it once per phase closure. Build a new
Fold<D::N2, MapH, MapR> whose init, accumulate, and
finalize delegate to the original through the captured clones.
Worked example
NoteVisits increments a shared counter every time init
runs. No type changes; grow and treeish pass through; fold
gets a wrapped init.
#![allow(unused)]
fn main() {
#[test]
fn custom_lift_note_visits() {
use std::sync::{Arc, Mutex};
use hylic::domain::Shared;
use hylic::domain::shared::fold::{self as sfold, Fold};
use hylic::graph::Treeish;
use hylic::ops::Lift;
/// Counts init calls into a shared counter.
#[derive(Clone)]
struct NoteVisits {
counter: Arc<Mutex<u64>>,
}
impl<N, H, R> Lift<Shared, N, H, R> for NoteVisits
where N: Clone + 'static, H: Clone + 'static, R: Clone + 'static,
{
type N2 = N;
type MapH = H;
type MapR = R;
fn apply<T>(
&self,
treeish: Treeish<N>,
fold: Fold<N, H, R>,
cont: impl FnOnce(
Treeish<N>,
Fold<N, H, R>,
) -> T,
) -> T
{
let fold_for_init = fold.clone();
let fold_for_acc = fold.clone();
let fold_for_fin = fold;
let counter = self.counter.clone();
let wrapped: Fold<N, H, R> = sfold::fold(
move |n: &N| { *counter.lock().unwrap() += 1; fold_for_init.init(n) },
move |h: &mut H, r: &R| fold_for_acc.accumulate(h, r),
move |h: &H| fold_for_fin.finalize(h),
);
cont(treeish, wrapped)
}
}
use hylic::ops::LiftBare;
use hylic::prelude::{treeish, fold, FUSED};
let counter = Arc::new(Mutex::new(0u64));
let lift = NoteVisits { counter: counter.clone() };
let t = treeish(|n: &u64| if *n > 0 { vec![*n - 1] } else { vec![] });
let f = fold(|n: &u64| *n, |h: &mut u64, c: &u64| *h += c, |h: &u64| *h);
let r: u64 = lift.run_on(&FUSED, t, f, &3u64);
assert_eq!(r, 6); // 3 + 2 + 1 + 0
assert_eq!(*counter.lock().unwrap(), 4); // four init calls
}
}
Apply via LiftBare::run_on or compose into a pipeline:
#![allow(unused)]
fn main() {
use hylic_pipeline::prelude::*;
let r = my_treeish_pipeline.lift()
.then_lift(NoteVisits { counter })
.run_from_node(&FUSED, &root);
}
When ShapeLift is sufficient
If the transformation is “rewrite one of the three slots” —
which it is most of the time — one of the per-axis primitives
or the universal ShapeLift does the job.
| Primitive | When |
|---|---|
Shared::phases_lift(mi, ma, mf) | rewrite all three Fold phases |
Shared::treeish_lift(mt) | rewrite the graph |
Shared::n_lift(lift_node, build_treeish, contra) | coordinated N-change across all slots |
Shared::wrap_init_lift(w) | wrap init |
Shared::zipmap_lift(m) | extend R |
Shared::filter_edges_lift(p) | drop edges from the graph |
(Local mirrors are alongside.) NoteVisits above is
expressible as
Shared::wrap_init_lift(|n, orig| { counter.bump(); orig(n) });
the custom impl was shown to illustrate the trait structure.
Capability bounds
PureLift—Clone + 'staticon the lift,Cloneon every output type. Required for the sequentialFusedexecutor.ShareableLift— addsSend + Sync + 'staticon the lift and on every payload. Required for the parallelFunnelexecutor.
Both are blanket markers; the compiler selects them when the
bounds are met. To run under Funnel, the lift struct itself
must be Clone + Send + Sync + 'static, and every captured
field must satisfy the same.