Reflection & Inversion

Orlando pipelines are inspectable, serializable, and — for the bijective subset — reversible. This chapter covers the three complementary layers that let you "break down data the way Orlando composes it."

For the full design rationale, see docs/INVERSE_TRANSDUCER_DESIGN.md.

LayerQuestion it answersWho can use it
C — Reflection"What stages does this pipeline have?"Any transducer
A — Inversion"Undo this transformation."The bijective subset only
B — Provenance"Which inputs produced these outputs?"Any transducer (needs the source)

Layer C: Reflection (describe)

Every built-in transducer implements Describable and can report its stages as a serializable Vec<StageSpec>.

Rust

#![allow(unused)]
fn main() {
use orlando_transducers::{Describable, StageSpec};
use orlando_transducers::transforms::{Map, Filter, Take};
use orlando_transducers::transducer::Transducer;

let p = Map::new(|x: i32| x * 2)
    .compose(Filter::new(|x: &i32| *x > 5))
    .compose(Take::new(3));

assert_eq!(p.describe(), vec![
    StageSpec::Map,
    StageSpec::Filter,
    StageSpec::Take { n: 3 },
]);
}

StageSpec carries captured parameters where meaningful (Take { n }, Chunk { size }, …) and is structural metadata only — it never includes the closure body (closures are not serializable). Each variant has a stable name() (e.g. "map", "takeWhile") for UI/serialization, and is_reversible() classifies the groupoid-eligible ops.

JavaScript

const p = new Pipeline().map(x => x * 2).filter(x => x > 5).take(3);
p.describe();
// [ { op: 'map' }, { op: 'filter' }, { op: 'take', n: 3 } ]

The descriptor is JSON-serializable, enabling pipeline-as-config and round-tripping through storage or a UI.

Declarative construction (pipeline! macro)

The pipeline! macro builds a chain with natural >> syntax and needs no struct imports (it is fully $crate-qualified and uses UFCS for compose). The result is both runnable and describable.

#![allow(unused)]
fn main() {
use orlando_transducers::pipeline;

let p = pipeline!(map(|x: i32| x * 2) >> filter(|x: &i32| *x > 5) >> take(3));
// p implements both Transducer and Describable.
}

A companion pipeline_descriptor! emits a compile-time, zero-allocation &'static [StageSpec] for the same stages:

#![allow(unused)]
fn main() {
use orlando_transducers::{pipeline_descriptor, StageSpec};

const DESC: &[StageSpec] = pipeline_descriptor!(map >> filter >> take(3));
assert_eq!(DESC, &[StageSpec::Map, StageSpec::Filter, StageSpec::Take { n: 3 }]);
}

Because both macros dispatch through the same per-operation mapping, and a pipeline!-built chain's .describe() is tested against the matching pipeline_descriptor!, forward composition and decomposition cannot drift.

Layer A: Inversion (Invertible / IsoMap)

Most pipelines are lossyFilter drops elements, Take truncates, FlatMap fans out — and destroy information, so they have no true inverse. The bijective subset (the groupoid) can be reversed.

The IsoMap type

IsoMap is a streaming isomorphism pairing a to and from function — the transducer analogue of the Iso optic, lifted to streams.

#![allow(unused)]
fn main() {
use orlando_transducers::invert::{Invertible, IsoMap};
use orlando_transducers::collectors::to_vec;

// Celsius ⇄ Fahrenheit
let to_f = IsoMap::new(|c: f64| c * 9.0 / 5.0 + 32.0, |f: f64| (f - 32.0) * 5.0 / 9.0);

let celsius = vec![0.0, 100.0, 25.0];
let fahrenheit = to_vec(&to_f, celsius.clone());      // [32, 212, 77]
let recovered  = to_vec(&to_f.invert(), fahrenheit);  // [0, 100, 25]
}

Caller responsibility: to and from must be true inverses.

Composition reverses by the groupoid law

(a ∘ b)⁻¹ = b⁻¹ ∘ a⁻¹. A composed pipeline inverts by reversing stage order and inverting each part:

#![allow(unused)]
fn main() {
use orlando_transducers::invert::IsoMap;
use orlando_transducers::transducer::Transducer;
use orlando_transducers::collectors::to_vec;

let a = IsoMap::new(|x: i32| x * 2, |y: i32| y / 2);
let b = IsoMap::new(|x: i32| x + 10, |y: i32| y - 10);
let forward = a.compose(b);

let input = vec![1, 2, 3];
let output = to_vec(&forward, input.clone());        // [12, 14, 16]
let recovered = to_vec(&forward.invert(), output);   // [1, 2, 3]
}

Excluded by construction

Lossy transducers (Filter, Take, Drop, Unique, FlatMap, plain Map, …) do not implement Invertible, so calling .invert() on a pipeline that contains them is a compile error. The groupoid excludes the lossy majority by construction — you cannot accidentally invert something non-invertible.

JavaScript

const toF = new Pipeline()
  .isoMap(c => c * 9/5 + 32, f => (f - 32) * 5/9)
  .isoMap(x => x + 10, y => y - 10);

if (toF.canInvert()) {
  const toC = toF.invert();   // reverses order, swaps to/from
}
// A pipeline with a lossy stage throws on invert():
// new Pipeline().filter(x => x > 0).invert();  // Error

canInvert() returns true only when every stage is an isoMap (or the pipeline is empty — the identity, which is self-inverse).

Layer B: Provenance (trace)

For lossy pipelines, the post-hoc "inverse" answers which inputs produced these outputs? The trace function records a tape mapping each output to its source input index.

#![allow(unused)]
fn main() {
use orlando_transducers::provenance::trace;
use orlando_transducers::transforms::{Map, Filter};
use orlando_transducers::transducer::Transducer;

let p = Filter::new(|x: &i32| x % 2 == 0).compose(Map::new(|x: i32| x * 2));
let data = vec![1, 2, 3, 4, 5, 6];

let (outputs, t) = trace(&p, data.clone());
assert_eq!(outputs, vec![4, 8, 12]);
// outputs[0] came from input[1], etc.
assert_eq!(t.sources, vec![1, 3, 5]);

// The "inverse of Filter": a boolean mask over the original input.
assert_eq!(t.kept_mask(data.len()), vec![false, true, false, true, false, true]);
}

Per-op provenance semantics:

OpProvenance
Map / Filteroutput tagged with its producing input
FlatMapevery fanned-out output shares the originating input's index
Chunk / Aperturethe emitted group tagged with its completing input
Take / Dropstandard prefix / suffix indexing

Unlike inversion, provenance is post-hoc — it needs the source stream because that is where the lost information lives. trace works on any transducer, lossy or invertible.