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 system

A domain controls how fold closures are stored — the boxing strategy that determines the refcount overhead, thread-safety, and transformation semantics of a Fold<N, H, R>. Graph types are domain-independent and live in a separate module (hylic::graph).

The three domains

The three built-in domains form a spectrum from maximum capability to minimum overhead:

%3sharedSharedArc<dyn Fn + Send + Sync>Clone, Send, SynclocalLocalRc<dyn Fn>Clone, !Sendshared->localnon-atomicrefcountownedOwnedBox<dyn Fn>!Clone, !Sendlocal->ownedno refcount

DomainFold storageCloneSend+SyncFold transformsExecutors
SharedArc<dyn Fn + Send + Sync>yesyesborrow (&self)Fused, Funnel
LocalRc<dyn Fn>yesnoborrow (&self)Fused
OwnedBox<dyn Fn>nonomove (self)Fused

The domain affects only the fold. Graph types (Treeish, Edgy, Graph) are always Arc-based because graph composition requires Clone. The executor accepts any graph type that implements TreeOps<N> — the graph’s storage is checked at the call site, not through the domain.

Module structure

Each domain module provides fold constructors and executor bindings. Graph types are in a separate public module:

domain/
  shared/fold.rs    Fold (Arc) + fold(), exec(), FUSED
  local/mod.rs      Fold (Rc)  + fold(), exec(), FUSED
  owned/mod.rs      Fold (Box) + fold(), exec(), FUSED

graph/
  edgy.rs           Edgy<N,E>, Treeish<N> (Arc) + combinators
  compose.rs        Graph

A typical program imports the prelude — every domain marker, the Shared-default constructors, and the graph constructors come with it:

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

For Local or Owned construction, address the per-domain module directly (hylic::domain::local, hylic::domain::owned) — see Import patterns.

The Domain trait

The Domain trait provides a single associated type — the concrete Fold type for each domain:

#![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>;
}
}

The Executor trait is parameterized by D: Domain<N>, so the compiler resolves D::Fold<H, R> to the concrete fold type at monomorphization time. The graph type is a separate type parameter G: TreeOps<N> on the Executor trait, constrained per executor implementation (Fused accepts any G; Funnel requires G: Send+Sync).

#![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;
}
}

FoldOps and TreeOps

The operations traits provide the universal interface that executors program against:

%3foldopsFoldOps<N, H, R>init / accumulate / finalizesfshared::Fold (Arc)foldops->sflflocal::Fold (Rc)foldops->lfofowned::Fold (Box)foldops->ofufuser structfoldops->uftreeopsTreeOps<N>visit / applygtgraph::Treeish (Arc)treeops->gtutuser structtreeops->ut

Any type implementing init/accumulate/finalize is a fold. Any type implementing visit is a graph. The executor’s recursion engine operates on these traits, not on concrete types.

Why the domain is on the executor

Fold<N, H, R> has no domain parameter — the domain is a type parameter 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 placing the domain on the fold would prevent the compiler from inferring the domain from the argument types. With D on the executor, each constant (shared::FUSED, local::FUSED, owned::FUSED) or <domain>::exec(...) call fixes D, and the compiler resolves everything statically. See Domain integration.

Choosing a domain

Shared is the default choice. It supports parallel execution (Funnel requires Send+Sync folds), lift integration (Explainer operates on Shared folds), and non-destructive fold transformations (the original fold is preserved after map/contramap/product).

Local provides the same transformation API with lighter refcounting (Rc vs Arc — non-atomic vs atomic increment). It works with the Fused executor for single-threaded computation.

Owned eliminates refcounting entirely. Fold transformations consume the original (move semantics). Useful for measuring the framework’s raw overhead in benchmarks, or for single-use folds where the original is not needed after transformation.

All three domains provide the same fold combinator surface: wrap_init, wrap_accumulate, wrap_finalize, map, zipmap, contramap, product. The difference is in the calling convention (borrow vs move) and the auto-traits (Send+Sync vs neither).