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

Arrow Combinators

Quiver’s Layer 1 provides Arrow-style combinators for composing DSP modules with compile-time type safety.

The Core Abstraction

Every module is a function from input to output:

\[ M : \text{In} \to \text{Out} \]

Combinators let us build complex modules from simple ones without losing type safety.

Chain (Sequential Composition)

The most fundamental combinator: output of first feeds input of second.

flowchart LR
    IN[Input] --> A[Module A]
    A --> B[Module B]
    B --> OUT[Output]

\[ \text{chain}(f, g) = g \circ f : A \to C \]

In code the method is then (producing a Chain value):

let synth = vco.then(vcf).then(vca);
// () → f64 → f64 → f64
// Types flow through automatically

Parallel (Independent Processing)

Process two signals independently:

flowchart LR
    subgraph Input
        I1[A]
        I2[C]
    end
    subgraph Processing
        M1[Module F]
        M2[Module G]
    end
    subgraph Output
        O1[B]
        O2[D]
    end

    I1 --> M1 --> O1
    I2 --> M2 --> O2

\[ (f \parallel g)(a, c) = (f(a), g(c)) \]

let stereo = left_channel.parallel(right_channel);
// (f64, f64) → (f64, f64)

Fanout (Split and Process)

Send input to multiple processors:

flowchart LR
    IN[Input A] --> SPLIT((•))
    SPLIT --> F[Module F]
    SPLIT --> G[Module G]
    F --> O1[B]
    G --> O2[C]

\[ \text{fanout}(f, g)(a) = (f(a), g(a)) \]

let effects = reverb.fanout(delay);
// f64 → (f64, f64): one input feeds both processors

First and Second

Apply a module to only one part of a pair:

flowchart LR
    subgraph "first(F)"
        I1[A] --> F[F]
        I2[X] --> P[Pass]
        F --> O1[B]
        P --> O2[X]
    end

\[ \text{first}(f)(a, x) = (f(a), x) \]

// Process only the left channel
let left_only = filter.first();
// (f64, f64) → (f64, f64)

Feedback (With Delay)

Create a feedback loop with unit delay:

flowchart LR
    IN[Input] --> SUM((+))
    SUM --> PROC[Process]
    PROC --> OUT[Output]
    PROC --> DEL[z⁻¹]
    DEL --> SUM

\[ y[n] = f(x[n] + y[n-1]) \]

feedback takes a closure called as combine(input, previous_output), where previous_output is the module’s output delayed by one sample:

// 50% feedback
let echo = delay.feedback(|input, previous| input + previous * 0.5);

Map and Contramap

Transform signals without creating new modules:

// Map: transform output
let boosted = vco.map(|x| x * 2.0);

// Contramap: transform input
let scaled = vca.contramap(|x| x * 0.5);
flowchart LR
    subgraph "map(f, g)"
        IN[A] --> M[Module]
        M --> TRANS[g]
        TRANS --> OUT[C]
    end

Identity

The do-nothing module—but type-safe:

let id = Identity::<f64>::new();
// f64 → f64, output equals input

// Useful for type alignment
let aligned = mono.parallel(Identity::new());

Constant

Always produce the same output:

let dc = Constant::new(5.0);
// () → f64, always 5.0

// Useful for fixed CV values
let offset = Constant::new(2.5).then(adder.second());

Split and Merge

Work with tuples:

// Split: duplicate input
let dup = Split::<f64>::new();
// f64 → (f64, f64)

// Merge: combine with function
let summer = Merge::new(|a, b| a + b);
// (f64, f64) → f64

Swap

Swap tuple elements:

let swapped = Swap::<f64, f64>::new();
// (A, B) → (B, A)

Combining Combinators

Build complex signal flow:

// Classic synth voice with stereo chorus
let voice = vco
    .then(vcf)
    .then(vca)
    .then(Split::new())  // Mono to stereo
    .then(
        chorus_left.parallel(chorus_right)
    )
    .then(
        Merge::new(|l, r| (l + r) * 0.5)  // Back to mono, averaged
    );

Type Inference

Rust’s type inference works through combinators:

// Types are inferred
let synth = vco.then(vcf).then(vca);
// Compiler knows: () → f64

// Explicit types when needed
let stereo: Chain<VCO, Parallel<VCF, VCF>> = ...;

Zero-Cost Abstraction

Combinators compile to efficient code:

// This combinator chain...
let synth = vco.then(vcf).then(vca);

// ...compiles to essentially:
fn tick(&mut self) -> f64 {
    self.vca.tick(
        self.vcf.tick(
            self.vco.tick(())
        )
    )
}

No heap allocation, no virtual dispatch, no runtime overhead.

Pattern: Effect Rack

// Statically chain a fixed rack of effects
let rack = distortion.then(chorus).then(delay).then(reverb);
// Each stage's Out type must match the next stage's In type

Pattern: Parallel Voices

fn parallel_voices<V: Module<In = f64, Out = f64>>(voices: [V; 4]) -> impl Module {
    let [v1, v2, v3, v4] = voices;
    // Note: parallel() pairs types, so this nests tuples:
    // In = (((f64, f64), f64), f64), and Out likewise
    v1.parallel(v2).parallel(v3).parallel(v4)
}