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

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.

PrimitiveWhen
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

  • PureLiftClone + 'static on the lift, Clone on every output type. Required for the sequential Fused executor.
  • ShareableLift — adds Send + Sync + 'static on the lift and on every payload. Required for the parallel Funnel executor.

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.