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)
}