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$$

let synth = vco.chain(vcf).chain(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 = signal.fanout(reverb, delay);
// f64 → (f64, f64)

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])$$

let echo = delay.feedback(0.5);  // 50% feedback

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).chain(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
    .chain(vcf)
    .chain(vca)
    .chain(Split::new())  // Mono to stereo
    .chain(
        chorus_left.parallel(chorus_right)
    )
    .chain(
        Merge::new(|l, r| (l * 0.5, r * 0.5))
    );

Type Inference

Rust’s type inference works through combinators:

// Types are inferred
let synth = vco.chain(vcf).chain(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.chain(vcf).chain(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

fn effect_rack(
    effects: Vec<Box<dyn Module<In=f64, Out=f64>>>
) -> impl Module<In=f64, Out=f64>
{
    effects.into_iter()
        .fold(Identity::new(), |acc, fx| acc.chain(fx))
}

Pattern: Parallel Voices

fn parallel_voices<V: Module<In=f64, Out=f64>>(
    voices: [V; 4]
) -> impl Module<In=(f64, f64, f64, f64), Out=(f64, f64, f64, f64)>
{
    let [v1, v2, v3, v4] = voices;
    v1.parallel(v2).parallel(v3).parallel(v4)
}