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

Domain integration

The domain system lets executors accept folds without knowing their concrete storage. The Domain trait maps a marker type to a concrete Fold type via a GAT. The graph type is a separate concern — the Executor trait accepts any G: TreeOps<N>, with per-executor bounds checked at the call site.

The Domain trait

The Domain trait provides a single associated type — the fold:

#![allow(unused)]
fn main() {
pub trait Domain<N: 'static>: 'static {
    type Fold<H: 'static, R: 'static>: FoldOps<N, H, R>;
    type Graph<E: 'static> where E: 'static;
    type Grow<Seed: 'static, NOut: 'static>;

    /// Construct a fold from three closures. Uniform Send+Sync
    /// bound; each domain sheds Send+Sync at storage time if it
    /// doesn't need it.
    fn make_fold<H: 'static, R: 'static>(
        init: impl Fn(&N) -> H + Send + Sync + 'static,
        acc:  impl Fn(&mut H, &R) + Send + Sync + 'static,
        fin:  impl Fn(&H) -> R + Send + Sync + 'static,
    ) -> Self::Fold<H, R>;

    /// Construct a grow closure from a Fn. Uniform Send+Sync bound.
    fn make_grow<Seed: 'static, NOut: 'static>(
        f: impl Fn(&Seed) -> NOut + Send + Sync + 'static,
    ) -> Self::Grow<Seed, NOut>;

    /// Invoke a stored grow closure.
    fn invoke_grow<Seed: 'static, NOut: 'static>(
        g: &Self::Grow<Seed, NOut>,
        s: &Seed,
    ) -> NOut;

    /// Construct a graph (Edgy) closure. Uniform Send+Sync bound.
    fn make_graph<E: 'static>(
        visit: impl Fn(&N, &mut dyn FnMut(&E)) + Send + Sync + 'static,
    ) -> Self::Graph<E>;
}
}

Each domain marker (Shared, Local, Owned) implements this trait with a different closure boxing strategy:

DomainFold<H, R> storageSend+Sync
SharedArc<dyn Fn + Send + Sync>yes
LocalRc<dyn Fn>no
OwnedBox<dyn Fn>no

Graph types are domain-independent. Treeish<N> and Edgy<N, E> in hylic::graph are always Arc-based (they need Clone for graph composition). Any type implementing TreeOps<N> can serve as a graph, including user-defined structs with no boxing at all.

The Executor trait

The executor trait has four type parameters: N (node), R (result), D (domain), and G (graph):

#![allow(unused)]
fn main() {
/// Run a fold on a tree. Both Specs and Sessions implement this.
///
/// The fold is domain-specific (`D::Fold<H, R>`). The graph type G
/// is a trait-level parameter — each executor impl declares its own
/// bounds on G (e.g. Fused accepts any TreeOps, Funnel requires
/// Send+Sync). The compiler checks G at the call site.
pub trait Executor<N: 'static, R: 'static, D: Domain<N>, G: TreeOps<N> + 'static> {
    /// Run the given `fold` over the `graph` starting at `root` and
    /// return the fold's final result for the root.
    fn run<H: 'static>(&self, fold: &D::Fold<H, R>, graph: &G, root: &N) -> R;
}
}

The domain D determines the fold type (D::Fold<H, R>). The graph type G is constrained per executor implementation. This separation means the fold’s boxing strategy and the graph’s storage are independent choices.

The type resolution at a call site proceeds as follows:

%3execExec<Shared, fused::Spec>= shared::FUSEDdomainD = Shared(fixed by const type)exec->domaintype param DgtypeG: TreeOps<N>(inferred from argument)exec->gtypeinferred fromgraph argumentnhrN, H, R(inferred from args)exec->nhrinferred fromfold + rootfoldD::Fold<H, R>= shared::Fold<N, H, R>domain->foldGAT resolution

The compiler checks that G satisfies the executor’s requirements. For Fused, any TreeOps<N> suffices. For Funnel, G must also be Send + Sync (the graph reference is shared across a scoped pool). If the graph type does not satisfy the executor’s bounds, the call site produces a compile error.

Why D is on the executor, not the fold

Fold<N, H, R> carries no domain parameter — the domain lives on the executor: Exec<D, S>. This resolves a type inference problem: GATs are not injective (D::Fold<H, R> does not uniquely identify D), so the compiler cannot infer D from a fold argument alone. With D fixed by the executor constant or exec() call, everything resolves statically.

Domain compatibility

SharedLocalOwned
Fusedyesyesyes
Funnelyes

Fused supports all domains because it borrows both fold and graph on a single thread. Funnel requires N: Clone + Send and R: Send on the fold’s types, which the Shared domain satisfies. The graph must additionally be Send + Sync.

The FoldOps trait

Executors do not call fold methods through the concrete domain type. They operate through the FoldOps<N, H, R> trait, which all domain Fold types implement:

%3foldopsFoldOps<N, H, R>init / accumulate / finalizesfshared::Fold (Arc)foldops->sflflocal::Fold (Rc)foldops->lfofowned::Fold (Box)foldops->of

The executor’s recursion engine takes &impl FoldOps<N, H, R> — fully monomorphized for the concrete fold type, with no runtime dispatch beyond the closure’s own vtable.