Arrow
Parsers/streaming, data pipelines
_ ____ ____ _____ ______
/ \ | _ \| _ \ / _ \ \ / / ___|
/ _ \ | |_) | |_) | | | \ \ /\ / /\___ \
/ ___ \| _ <| _ <| |_| |\ V V / ___) |
/_/ \_\_| \_\_| \_\\___/ \_/\_/ |____/
Back in arrow category we touched upon the Arrow abstraction. Now it is time to dive deeper into this underappreciated idea.
If monads are the celebrities of functional programming — with their own conferences, blog posts, and tutorial wars — then Arrows are the skilled craftspeople working quietly behind the scenes. They give a common interface to computations for which a monadic interface may be unnatural or too restrictive.[8]
Not all computations need the full power of monads, but many need more structure than functors provide.
This creates a sweet spot where Arrows shine:
- 🎯 Structured Computation: An explicit input-output interface for effectful or otherwise structured computations
- 🔄 Composition: Natural ways to combine and sequence computations
- 📊 Static Analysis: Structure that can be analyzed without execution
- ⚡ Optimization: Opportunities for program transformation and improvement
- 🧩 Domain Modeling: Perfect for circuits, parsers, streams, and state machines
These abstractions emphasize different forms of composition:
Functor mapping values in a context
Applicative combining context-independent computations
Arrow composing input-output computations
Monad choosing later computations from earlier values
Arrows are useful when you need:
- More than mapping: Your computations have explicit inputs and outputs
- Restricted dependency: Later stages do not need to construct arbitrary new stages from intermediate values
- Visible wiring: The chosen Arrow representation preserves enough structure to analyze, optimize, or visualize the computation
Monads support value-dependent composition: a result can determine which computation is constructed next. Basic Arrow notation instead fixes the computational stages and their wiring statically. Whether an Arrow value itself can be inspected depends on its concrete representation.
This difference unlocks several options:
- Introspection: A reified Arrow representation can be examined without being executed
- Optimization: A domain-specific Arrow can support transformations before interpretation
- Visualization: A reified computation graph can produce diagrams or analysis reports
- Testing: Arrow laws and domain-specific structure can be tested independently
Consider these real-world scenarios where Arrows excel:
- Circuit Design: Electrical or logical circuits with clear input/output flows
- Data Processing Pipelines: ETL operations with known transformation stages
- Protocol Parsers: Network protocols with structured parsing requirements
- Signal Processing: Audio/video processing with filter chains
- Configuration Systems: Complex configurations with validation and transformation
It is time to restrain our monadic powers a little and learn some computational discipline.
Arrow #
At its heart, an Arrow generalizes functions while retaining identity and composition. Where functions compose with (.), Arrow computations form a Category and gain combinators for routing product-shaped inputs.[7]
Arrows capture the essence of "structured computation" — computations that have a clear input-output relationship but may involve complex internal transformations, effects, or computational context. They are not literally an intermediate point in a single hierarchy between functions and monads; they provide a different interface to composition.
In addition to pure transformations, Arrow instances can represent computations involving state or failure. Extra capabilities such as branching and feedback are expressed by subclasses including ArrowChoice and ArrowLoop.
Formal Definition #
In Haskell, an Arrow is a binary type constructor whose minimal operations are arr and either first or (***), together with the identity and composition supplied by Category. The remaining combinators have default definitions.[6]
class Category arr => Arrow arr where
arr :: (b -> c) -> arr b c
first :: arr b c -> arr (b, d) (c, d)
second :: arr b c -> arr (d, b) (d, c)
(***) :: arr b c -> arr b' c' -> arr (b, b') (c, c')
(&&&) :: arr b c -> arr b c' -> arr b (c, c')
The Category constraint requires:
class Category arr where
id :: arr a a -- Identity arrow
(.) :: arr b c -> arr a b -> arr a c -- Arrow composition
Arrow laws:[10]
- Identity preservation:
arr id = id - Composition preservation:
arr (f >>> g) = arr f >>> arr g - Pure-product compatibility:
first (arr f) = arr (first f) - Composition under
first:first (f >>> g) = first f >>> first g - Projection coherence:
first f >>> arr fst = arr fst >>> f - Naturality in the untouched component:
first f >>> arr (id *** g) = arr (id *** g) >>> first f - Associativity coherence:
first (first f) >>> arr assoc = arr assoc >>> first f
Here assoc ((a, b), c) = (a, (b, c)). In laws 3 and 6, the operations inside arr are the corresponding operations on ordinary functions.
These laws ensure that Arrow operations interact coherently with pure functions, composition, and products.
The Arrow methods work as follows:
arr: Lifts any pure function into the Arrow contextfirst: Takes an Arrow and makes it operate on the first component of a pair, leaving the second component unchangedsecond: Similarly operates on the second component of a pair(***): Routes the two components of a pair through two Arrows and pairs their outputs(&&&): Sends one input through two Arrows and pairs their outputsid: The do-nothing Arrow that passes input through unchanged(.): Composes Arrows sequentially, like function composition but preserving Arrow structure
These operations are sufficient to build complex computational networks. Static analyzability is not guaranteed by the Arrow class alone; it is a property of representations that retain the network as data.
Categorically, an Arrow can be described as an internally strong monoid in a category of profunctors.
Product category #
A product category is a fundamental construction in category theory that allows us to combine two categories into a single, larger category. Understanding product categories is crucial for grasping how Arrows work categorically.[1]
Given two categories C and D, their product category C × D has:
- Objects: Pairs
(A, B)whereAis an object in C andBis an object in D - Morphisms: Pairs
(f, g) : (A, B) → (A', B')wheref : A → A'in C andg : B → B'in D - Composition:
(f', g') ∘ (f, g) = (f' ∘ f, g' ∘ g) - Identity:
id_{(A,B)} = (id_A, id_B)
Product categories satisfy a universal property: for any category E and functors F : E → C and G : E → D, there exists a unique functor ⟨F, G⟩ : E → C × D such that:
π₁ ∘ ⟨F, G⟩ = F
π₂ ∘ ⟨F, G⟩ = G
This universality makes C × D the categorical product of C and D. For Arrows, the relevant construction is C^op × C: its objects are input-output pairs.
Connection to Haskell types:
For the category of Haskell types and functions:
- Objects of
Hask^op × Hask: Pairs of types(A, B) - Morphisms: Pairs of functions, with the first component reversed by
Hask^op - Arrow computations:
arr A Bis the collection, represented by a Haskell type, assigned to the input-output pair(A, B)
The product-category domain accounts for the contravariant input and covariant output of arr A B. The Arrow operations require additional structure beyond the product category itself.
Categorical Arrow #
Let C be a category of pure computations with finite products. The carrier of an Arrow is a profunctor:
A : C^op × C → Set
Here:
C^op × Cis the product of the opposite category and the original categoryC^ophas the same objects asCbut all arrows reversedA(X, Y)is the set of Arrow computations fromXtoY
The pair (X, Y) has:
Xin the contravariant input positionYin the covariant output position
The profunctor is only the carrier. An Arrow also has:[9]
- A unit that embeds pure morphisms, corresponding to
arr - An associative multiplication, corresponding to Arrow composition
- An internal strength, corresponding to carrying an untouched value alongside a computation with
first - The Arrow laws, which make these operations coherent
Equivalently, an Arrow is an internally strong monoid in the monoidal category of profunctors on C, where the tensor is profunctor composition. This is the precise sense in which Arrows, like monads, are monoids.
Profunctorial structure:
The type constructor arr b c is contravariant in its first argument and covariant in its second:
- Contravariant in the first argument (input type)
- Covariant in the second argument (output type)
These mappings can be derived by composing with lifted pure functions:
lmap f p = arr f >>> p
rmap g p = p >>> arr g
Monoidal Structure #
We are going to need to brush up on our monoidal category knowledge.
A monoidal category (C, ⊗, I) consists of:[3]
- A category
C - A bifunctor
⊗ : C × C → C(the monoidal product) - An object
I(the unit object) - Natural isomorphisms for associativity, left unit, and right unit
Ignoring Haskell's nontermination and other semantic complications, the usual model treats Hask as cartesian monoidal:
- Category: Hask (types and functions)
- Product:
(×)(tuple formation) - Unit:
()(unit type)
An Arrow determines a category of computations with:
- Objects: The objects of the underlying pure category
- Morphisms: Elements of
A(X, Y), represented in Haskell by values of typearr x y - Identity:
arr id - Composition:
(>>>)
In general, this computation category is premonoidal, not monoidal: the order of effects may matter. The operations first and second tensor an effectful computation with an identity computation on either side:
first : arr(A,B) → arr(A×C, B×C)
second : arr(A,B) → arr(C×A, C×B)
Together with the identity-on-objects functor that embeds pure computations via arr, this forms a Freyd category. The internal strength in the profunctor formulation and the premonoidal structure in the Freyd-category formulation express the same Arrow structure.
Freyd Category #
Before defining a Freyd category, we need two supporting ideas.
A premonoidal category resembles a monoidal category, but its tensor need only be functorial in each argument separately. Given computations f : X → X' and g : Y → Y', applying f and then g need not equal applying g and then f. This distinction models the observable order of effects.
A morphism f : X → X' is central when it commutes with every morphism g : Y → Y' acting on another component. In particular, the following two execution orders must agree:
(f ⊗ id_Y) >>> (id_X' ⊗ g) = (id_X ⊗ g) >>> (f ⊗ id_Y')
The analogous equation must also hold with f acting on the right component. Pure computations are central because moving them across an independent effect does not change the effect's observable order.
Strictly, a Freyd category consists of:[11]
- A category
Cwith finite products, representing values and pure computations - A symmetric premonoidal category
D, representing possibly effectful computations - An identity-on-objects functor
J : C → Dthat strictly preserves the premonoidal structure and maps every morphism ofCto a central morphism ofD
Because J is identity-on-objects, C and D have the same objects but may have different morphisms. Strict preservation means, in particular, that:
J(X × Y) = X ⊗ YJ(1) = I, where1andIare the respective unit objectsJpreserves the associators, unitors, and symmetry on the nose
An Arrow A over C produces such a Freyd category:
- The objects of
Dare the objects ofC D(X, Y) = A(X, Y)- The identity in
Disarr id - Composition in
Dis(>>>) Jacts on morphisms asJ(f) = arr f- Tensoring an Arrow with an identity is given by
firstorsecond
The Arrow laws ensure that these definitions are coherent and that lifted pure morphisms are central. Conversely, under the standard size assumptions, a Freyd category determines an Arrow.
Lax Monoidal Functors #
A lax monoidal functor between monoidal categories (C, ⊗, I) and (D, ⊛, J) consists of:[2][4]
- A functor
F : C → D - A morphism
φ₀ : J → F(I) - A natural family
φ₂ : F(A) ⊛ F(B) → F(A ⊗ B) - Coherence laws relating these maps to associativity and the unitors
The word "lax" means that φ₀ and φ₂ need not be isomorphisms. It does not imply that values or components are necessarily lost.
Applicative functors as lax monoidal functors. We have seen this correspondence as well.
Applicative functors correspond to lax monoidal endofunctors on Hask with its cartesian monoidal structure:
class Functor f => Applicative f where
pure :: a -> f a -- pure () represents φ₀: () → f ()
(<*>) :: f (a -> b) -> f a -> f b -- Derived from φ₂
The lax monoidal structure is:
-- φ₀: Unit preservation
pure :: () -> f ()
-- φ₂: Lax product preservation
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
-- Equivalently: f a × f b → f (a × b)
The map combines two independent effects into one product-shaped effect. The Applicative interface does not allow the result of the first effect to choose the second effect.
Strong Monoidal Functors #
A strong monoidal functor is a lax monoidal functor for which the structure maps are isomorphisms:[5]
φ₀ : J → F(I)is an isomorphismφ₂ : F(A) ⊛ F(B) → F(A ⊗ B)is a natural isomorphism
Their inverses provide maps in the other direction, such as:
F(A ⊗ B) → F(A) ⊛ F(B)
Despite the similar terminology, a Haskell Arrow is not, in general, a strong monoidal functor. Its "internal strength" is a different categorical notion. In particular, (***) does not imply parallel execution: its usual definition sequences first f and second g, and effects need not commute.
The Hierarchy #
Applicatives, Arrows, and monads are related, but they do not form a simple linear hierarchy:
Applicative: combines independent contextual computations
Monad: supports value-dependent sequencing
Arrow: composes computations with explicit input and output
- Every monad
minduces a Kleisli ArrowKleisli m a b = a → m b - Some useful Arrows do not arise naturally from a monadic interface
- Applicative and Arrow constrain dependency in different ways and operate on type constructors of different arities
It is therefore reasonable to describe Arrows as more structurally constrained than monadic code, but not as strong monoidal functors sitting between Applicative and Monad.
Arrows are internally strong monoids in a suitable category of profunctors; equivalently, they correspond to Freyd categories under standard assumptions.
Operationally, the basic interface provides:
- Lifting pure functions (
arr) - Operating on products (
first/second) - Preserving categorical structure (composition, identity)
These capabilities make Arrows useful for structured computations with explicit data flow. Whether a particular Arrow supports inspection, optimization, parallelism, choice, or feedback depends on its representation and subclasses.
Arrow Subclasses #
The basic Arrow class provides composition and product-shaped routing. Additional type classes introduce capabilities that not every Arrow can support coherently.
ArrowChoice #
ArrowChoice adds branching over sum types, represented in Haskell by Either:
class Arrow arr => ArrowChoice arr where
left :: arr b c -> arr (Either b d) (Either c d)
right :: arr b c -> arr (Either d b) (Either d c)
(+++) :: arr b c -> arr b' c' -> arr (Either b b') (Either c c')
(|||) :: arr b d -> arr c d -> arr (Either b c) d
The operations have the following roles:
left fappliesfto aLeftvalue and passes aRightvalue through unchangedright fis the mirror image ofleft ff +++ groutesLeftvalues throughfandRightvalues throughgf ||| ghandles both alternatives and merges their outputs into one type
The branch is selected by the input's Either tag. The Arrow network still specifies both branches in advance; it does not construct a new Arrow from an intermediate result.
ArrowLoop #
ArrowLoop adds feedback:
class Arrow arr => ArrowLoop arr where
loop :: arr (b, d) (c, d) -> arr b c
The wrapped Arrow receives an ordinary input b and a feedback input d, then produces an ordinary output c and a new feedback value d. loop ties the feedback output to the feedback input:
┌───────────────┐
b ──>│ │──> c
│ f │
│ d ──┼─┐
│ ^ │ │
└───────────│───┘ │
└─────┘
This is a fixed-point or feedback construction, not necessarily repeated execution. It is useful for circuits, signal functions, and recursive data flow. Not every Arrow admits a lawful loop; the instance must give a meaningful interpretation to the feedback path.
ArrowApply #
ArrowApply allows an Arrow value itself to arrive as input and then be applied dynamically:
class Arrow arr => ArrowApply arr where
app :: arr (arr b c, b) c
The input to app is a pair containing an Arrow arr b c and a value b. The supplied Arrow is chosen at runtime and applied to that value to produce c.
This capability crosses the boundary established by basic Arrow notation. The computation graph no longer needs to be fixed in advance because an intermediate value can determine which Arrow runs next. In this precise sense, ArrowApply has the expressive power of monadic computation: every monad produces a Kleisli Arrow with application, and an ArrowApply instance produces a monad through the ArrowMonad construction.
Use ArrowApply when dynamic application is essential. If it appears throughout a design, a direct monadic formulation may be simpler and may communicate the dependency structure more clearly.
Examples #
Circuit Processing with Arrows #
One of the most intuitive applications of Arrows is modeling computational circuits where data flows through interconnected processing units.
{-# LANGUAGE Arrows #-}
import Control.Arrow
import Control.Category
import Prelude hiding (id, (.))
-- Define a simple Arrow for signal processing
newtype Circuit a b = Circuit { runCircuit :: a -> b }
instance Category Circuit where
id = Circuit id
Circuit f . Circuit g = Circuit (f . g)
instance Arrow Circuit where
arr = Circuit
first (Circuit f) = Circuit $ \(a, c) -> (f a, c)
second (Circuit f) = Circuit $ \(c, a) -> (c, f a)
-- Basic circuit components
amplify :: Double -> Circuit Double Double
amplify gain = arr (* gain)
delay :: Circuit Double Double
delay = arr id -- Simplified delay
signalFilter :: Double -> Circuit Double Double
signalFilter cutoff = arr $ \x -> if abs x > cutoff then x else 0
-- More complex circuits using Arrow combinators
parallelAmplifiers :: Circuit (Double, Double) (Double, Double)
parallelAmplifiers = amplify 2.0 *** amplify 3.0
serialProcessing :: Circuit Double Double
serialProcessing = amplify 1.5 >>> signalFilter 0.1 >>> delay
-- Branching and rejoining signals
splitterMixer :: Circuit Double Double
splitterMixer = proc input -> do
amplified <- amplify 2.0 -< input
filtered <- signalFilter 0.5 -< input
returnA -< amplified + filtered
-- Multi-stage processing pipeline
audioProcessor :: Circuit Double Double
audioProcessor = proc signal -> do
-- Stage 1: Pre-amplification
preamp <- amplify 1.2 -< signal
-- Stage 2: Parallel processing
(highGain, lowGain) <- (amplify 3.0 *** amplify 0.8) -< (preamp, preamp)
-- Stage 3: Filtering
filtered <- signalFilter 0.3 -< highGain
-- Stage 4: Mix and final amplification
mixed <- arr (uncurry (+)) -< (filtered, lowGain)
final <- amplify 0.9 -< mixed
returnA -< final
-- Example usage functions
main :: IO ()
main = do
putStrLn "Testing audio processor:"
print $ runCircuit audioProcessor 1.0
putStrLn "Testing serial processing:"
print $ runCircuit serialProcessing 2.5
putStrLn "Testing parallel amplifiers:"
print $ runCircuit parallelAmplifiers (1.0, 2.0)
putStrLn "Testing splitter mixer:"
print $ runCircuit splitterMixer 1.5
-- Direct usage examples:
-- runCircuit audioProcessor 1.0 -- Returns: 4.104
-- runCircuit serialProcessing 2.5 -- Returns: 3.75
-- runCircuit parallelAmplifiers (1.0, 2.0) -- Returns: (2.0, 6.0)
While TypeScript doesn't have native Arrow support, we can implement similar patterns using function composition and higher-order functions:
// Circuit type mimicking Arrow behavior
interface Circuit<A, B> {
run: (input: A) => B;
}
// Helper function to create circuits (similar to 'arr')
function circuit<A, B>(f: (input: A) => B): Circuit<A, B> {
return { run: f };
}
// Function composition operator (similar to '>>>')
function compose<A, B, C>(
first: Circuit<A, B>,
second: Circuit<B, C>
): Circuit<A, C> {
return circuit(input => second.run(first.run(input)));
}
// Parallel composition (similar to '***')
function parallel<A, B, C, D>(
left: Circuit<A, B>,
right: Circuit<C, D>
): Circuit<[A, C], [B, D]> {
return circuit(([a, c]) => [left.run(a), right.run(c)]);
}
// Basic circuit components
const amplify = (gain: number): Circuit<number, number> =>
circuit(input => input * gain);
const delay: Circuit<number, number> =
circuit(input => input); // Simplified delay
const signalFilter = (cutoff: number): Circuit<number, number> =>
circuit(input => Math.abs(input) > cutoff ? input : 0);
// Complex circuits using composition
const parallelAmplifiers: Circuit<[number, number], [number, number]> =
parallel(amplify(2.0), amplify(3.0));
const serialProcessing: Circuit<number, number> =
compose(
compose(amplify(1.5), signalFilter(0.1)),
delay
);
// Branching and rejoining signals
const splitterMixer: Circuit<number, number> = circuit(input => {
const amplified = amplify(2.0).run(input);
const filtered = signalFilter(0.5).run(input);
return amplified + filtered;
});
// Multi-stage processing pipeline
const audioProcessor: Circuit<number, number> = circuit(signal => {
// Stage 1: Pre-amplification
const preamp = amplify(1.2).run(signal);
// Stage 2: Parallel processing
const [highGain, lowGain] = parallelAmplifiers.run([preamp, preamp]);
// Stage 3: Filtering
const filtered = signalFilter(0.3).run(highGain);
// Stage 4: Mix and final amplification
const mixed = filtered + lowGain;
const final = amplify(0.9).run(mixed);
return final;
});
// Example usage
function testCircuits() {
console.log("Testing audio processor:");
console.log(audioProcessor.run(1.0)); // 4.104
console.log("Testing serial processing:");
console.log(serialProcessing.run(2.5)); // 3.75
console.log("Testing parallel amplifiers:");
console.log(parallelAmplifiers.run([1.0, 2.0])); // [2.0, 6.0]
console.log("Testing splitter mixer:");
console.log(splitterMixer.run(1.5)); // 4.5
}
// Advanced: Arrow-like operators using fluent interface
class CircuitBuilder<A, B> {
constructor(private circuit: Circuit<A, B>) {}
// Composition operator
then<C>(next: Circuit<B, C>): CircuitBuilder<A, C> {
return new CircuitBuilder(compose(this.circuit, next));
}
// Parallel composition with another circuit
and<C, D>(other: Circuit<C, D>): CircuitBuilder<[A, C], [B, D]> {
return new CircuitBuilder(parallel(this.circuit, other));
}
// Run the circuit
run(input: A): B {
return this.circuit.run(input);
}
}
// Factory function for fluent interface
function from<A, B>(f: (input: A) => B): CircuitBuilder<A, B> {
return new CircuitBuilder(circuit(f));
}
// Fluent interface example
const fluentProcessor = from((x: number) => x * 1.2)
.then(circuit(x => x > 0.3 ? x : 0))
.then(circuit(x => x * 0.9));
console.log("Fluent processor:", fluentProcessor.run(1.0)); // 1.08
C# can implement Arrow-like patterns using interfaces, generics, and extension methods to create composable circuit processing:
using System;
// Circuit interface mimicking Arrow behavior
public interface ICircuit<TInput, TOutput>
{
TOutput Run(TInput input);
}
// Concrete circuit implementation
public class Circuit<TInput, TOutput> : ICircuit<TInput, TOutput>
{
private readonly Func<TInput, TOutput> function;
public Circuit(Func<TInput, TOutput> function)
{
this.function = function;
}
public TOutput Run(TInput input) => function(input);
}
// Static factory and composition methods
public static class Circuit
{
// Helper to create circuits (similar to 'arr')
public static ICircuit<TInput, TOutput> From<TInput, TOutput>(
Func<TInput, TOutput> function)
{
return new Circuit<TInput, TOutput>(function);
}
// Function composition operator (similar to '>>>')
public static ICircuit<TInput, TOutput3> Compose<TInput, TOutput1, TOutput3>(
ICircuit<TInput, TOutput1> first,
ICircuit<TOutput1, TOutput3> second)
{
return From<TInput, TOutput3>(input => second.Run(first.Run(input)));
}
// Parallel composition (similar to '***')
public static ICircuit<(TInput1, TInput2), (TOutput1, TOutput2)> Parallel<TInput1, TInput2, TOutput1, TOutput2>(
ICircuit<TInput1, TOutput1> left,
ICircuit<TInput2, TOutput2> right)
{
return From<(TInput1, TInput2), (TOutput1, TOutput2)>(
inputs => (left.Run(inputs.Item1), right.Run(inputs.Item2)));
}
// Try the first circuit and use the second one if it fails.
public static ICircuit<TInput, TOutput> Alternative<TInput, TOutput>(
ICircuit<TInput, TOutput> first,
ICircuit<TInput, TOutput> second)
{
return From<TInput, TOutput>(input =>
{
try
{
return first.Run(input);
}
catch
{
return second.Run(input);
}
});
}
}
// Extension methods for fluent interface
public static class CircuitExtensions
{
public static ICircuit<TInput, TOutput3> Then<TInput, TOutput1, TOutput3>(
this ICircuit<TInput, TOutput1> first,
ICircuit<TOutput1, TOutput3> second)
{
return Circuit.Compose(first, second);
}
public static ICircuit<(TInput1, TInput2), (TOutput1, TOutput2)> And<TInput1, TInput2, TOutput1, TOutput2>(
this ICircuit<TInput1, TOutput1> left,
ICircuit<TInput2, TOutput2> right)
{
return Circuit.Parallel(left, right);
}
public static ICircuit<TInput, TResult> Map<TInput, TOutput, TResult>(
this ICircuit<TInput, TOutput> circuit,
Func<TOutput, TResult> selector)
{
return circuit.Then(Circuit.From(selector));
}
public static ICircuit<TInput, TOutput> Or<TInput, TOutput>(
this ICircuit<TInput, TOutput> first,
ICircuit<TInput, TOutput> second)
{
return Circuit.Alternative(first, second);
}
}
// Basic circuit components
public static class BasicCircuits
{
public static ICircuit<double, double> Amplify(double gain) =>
Circuit.From<double, double>(input => input * gain);
public static ICircuit<double, double> Delay =>
Circuit.From<double, double>(input => input); // Simplified delay
public static ICircuit<double, double> SignalFilter(double cutoff) =>
Circuit.From<double, double>(input => Math.Abs(input) > cutoff ? input : 0);
}
// Example usage class
public class CircuitProcessingExample
{
public static void Main()
{
// Complex circuits using composition
var parallelAmplifiers = BasicCircuits.Amplify(2.0)
.And(BasicCircuits.Amplify(3.0));
var serialProcessing = BasicCircuits.Amplify(1.5)
.Then(BasicCircuits.SignalFilter(0.1))
.Then(BasicCircuits.Delay);
// Branching and rejoining signals
var splitterMixer = Circuit.From<double, double>(input =>
{
var amplified = BasicCircuits.Amplify(2.0).Run(input);
var filtered = BasicCircuits.SignalFilter(0.5).Run(input);
return amplified + filtered;
});
// Multi-stage processing pipeline
var audioProcessor = Circuit.From<double, double>(signal =>
{
// Stage 1: Pre-amplification
var preamp = BasicCircuits.Amplify(1.2).Run(signal);
// Stage 2: Parallel processing
var (highGain, lowGain) = parallelAmplifiers.Run((preamp, preamp));
// Stage 3: Filtering
var filtered = BasicCircuits.SignalFilter(0.3).Run(highGain);
// Stage 4: Mix and final amplification
var mixed = filtered + lowGain;
var final = BasicCircuits.Amplify(0.9).Run(mixed);
return final;
});
// Test the circuits
TestCircuits(audioProcessor, serialProcessing, parallelAmplifiers, splitterMixer);
}
private static void TestCircuits(
ICircuit<double, double> audioProcessor,
ICircuit<double, double> serialProcessing,
ICircuit<(double, double), (double, double)> parallelAmplifiers,
ICircuit<double, double> splitterMixer)
{
Console.WriteLine("Testing audio processor:");
Console.WriteLine(audioProcessor.Run(1.0)); // 4.104
Console.WriteLine("Testing serial processing:");
Console.WriteLine(serialProcessing.Run(2.5)); // 3.75
Console.WriteLine("Testing parallel amplifiers:");
var (left, right) = parallelAmplifiers.Run((1.0, 2.0));
Console.WriteLine($"({left}, {right})"); // (2, 6)
Console.WriteLine("Testing splitter mixer:");
Console.WriteLine(splitterMixer.Run(1.5)); // 4.5
}
}
// Advanced: LINQ-style query syntax for circuits
public static class CircuitLinq
{
public static ICircuit<TInput, TResult> Select<TInput, TOutput, TResult>(
this ICircuit<TInput, TOutput> circuit,
Func<TOutput, TResult> selector)
{
return circuit.Then(Circuit.From(selector));
}
public static ICircuit<TInput, TResult> SelectMany<TInput, TOutput, TResult>(
this ICircuit<TInput, TOutput> circuit,
Func<TOutput, ICircuit<TInput, TResult>> selector)
{
return Circuit.From<TInput, TResult>(input =>
{
var intermediate = circuit.Run(input);
return selector(intermediate).Run(input);
});
}
}
// LINQ-style example
public class LinqCircuitExample
{
public static void Example()
{
var linqProcessor = BasicCircuits.Amplify(2.0)
.Select(x => x + 1)
.Then(BasicCircuits.SignalFilter(1.0))
.Select(x => x * 0.5);
Console.WriteLine($"LINQ-style processor: {linqProcessor.Run(1.0)}"); // 1.5
}
}
Parser Combinators with Arrows #
Arrows can build parsers with clear structure. This simplified example represents failure with Maybe; richer parser Arrows can retain error details:
import Control.Arrow
import Control.Category
import Data.Char (ord)
import Prelude hiding (id, (.))
-- Simple parser Arrow (simplified for demonstration)
newtype SimpleParser a b = SimpleParser { runSimpleParser :: a -> Maybe b }
instance Category SimpleParser where
id = SimpleParser Just
SimpleParser f . SimpleParser g = SimpleParser $ \input -> do
intermediate <- g input
f intermediate
instance Arrow SimpleParser where
arr f = SimpleParser (Just . f)
first (SimpleParser p) = SimpleParser $ \(a, c) -> do
b <- p a
return (b, c)
-- Basic parsing primitives for characters
charParser :: Char -> SimpleParser String Char
charParser target = SimpleParser $ \input -> case input of
[] -> Nothing
(c:_) -> if c == target then Just c else Nothing
digitParser :: SimpleParser Char Int
digitParser = SimpleParser $ \c ->
if c >= '0' && c <= '9'
then Just (ord c - ord '0')
else Nothing
-- Arrow-based parser combinators
parseSpecificDigit :: Char -> SimpleParser String Int
parseSpecificDigit d = charParser d >>> digitParser
-- Simple number parser (parses first character only for demo)
parseFirstDigit :: SimpleParser String Int
parseFirstDigit = SimpleParser $ \input -> case input of
[] -> Nothing
(c:_) -> if c >= '0' && c <= '9'
then Just (ord c - ord '0')
else Nothing
-- Working example functions
main :: IO ()
main = do
putStrLn "Testing parseFirstDigit:"
print $ runSimpleParser parseFirstDigit "5abc"
putStrLn "Testing charParser:"
print $ runSimpleParser (charParser 'a') "abc"
putStrLn "Testing parseSpecificDigit:"
print $ runSimpleParser (parseSpecificDigit '7') "7xyz"
-- Example usage:
-- runSimpleParser parseFirstDigit "123abc" returns Just 1
-- runSimpleParser (charParser 'a') "abc" returns Just 'a'
// Parser type mimicking Arrow behavior for parsing
interface Parser<A, B> {
parse: (input: A) => B | null;
}
// Helper function to create parsers (similar to 'arr')
function parser<A, B>(f: (input: A) => B | null): Parser<A, B> {
return { parse: f };
}
// Parser composition operator (similar to '>>>')
function composeParser<A, B, C>(
first: Parser<A, B>,
second: Parser<B, C>
): Parser<A, C> {
return parser(input => {
const intermediate = first.parse(input);
if (intermediate === null) return null;
return second.parse(intermediate);
});
}
// Parallel parser composition for tuples
function parallelParser<A, B, C, D>(
left: Parser<A, B>,
right: Parser<C, D>
): Parser<[A, C], [B, D]> {
return parser(([a, c]) => {
const leftResult = left.parse(a);
const rightResult = right.parse(c);
if (leftResult === null || rightResult === null) return null;
return [leftResult, rightResult];
});
}
// Basic parsing primitives
const charParser = (target: string): Parser<string, string> =>
parser(input => {
if (input.length === 0) return null;
const firstChar = input[0];
return firstChar === target ? firstChar : null;
});
const digitParser: Parser<string, number> =
parser(char => {
const code = char.charCodeAt(0);
const isDigit = code >= 48 && code <= 57; // '0' to '9'
return isDigit ? code - 48 : null;
});
// Combined parsers using composition
const parseSpecificDigit = (digit: string): Parser<string, number> =>
composeParser(charParser(digit), digitParser);
// Parse first character as digit
const parseFirstDigit: Parser<string, number> =
parser(input => {
if (input.length === 0) return null;
const firstChar = input[0];
const code = firstChar.charCodeAt(0);
const isDigit = code >= 48 && code <= 57;
return isDigit ? code - 48 : null;
});
// String consumer that advances position
interface StringState {
input: string;
position: number;
}
// Stateful parser for consuming input
interface StatefulParser<A> {
parse: (state: StringState) => { result: A; newState: StringState } | null;
}
function statefulParser<A>(f: (state: StringState) => { result: A; newState: StringState } | null): StatefulParser<A> {
return { parse: f };
}
// Character parser that advances position
const charStatefulParser = (target: string): StatefulParser<string> =>
statefulParser(state => {
if (state.position >= state.input.length) return null;
const char = state.input[state.position];
if (char !== target) return null;
return {
result: char,
newState: { input: state.input, position: state.position + 1 }
};
});
// Digit parser for stateful parsing
const digitStatefulParser: StatefulParser<number> =
statefulParser(state => {
if (state.position >= state.input.length) return null;
const char = state.input[state.position];
const code = char.charCodeAt(0);
const isDigit = code >= 48 && code <= 57;
if (!isDigit) return null;
return {
result: code - 48,
newState: { input: state.input, position: state.position + 1 }
};
});
// Composition for stateful parsers
function composeStateful<A, B>(
first: StatefulParser<A>,
second: (a: A) => StatefulParser<B>
): StatefulParser<B> {
return statefulParser(state => {
const firstResult = first.parse(state);
if (firstResult === null) return null;
const secondParser = second(firstResult.result);
return secondParser.parse(firstResult.newState);
});
}
// Sequential composition (ignoring first result)
function thenStateful<A, B>(
first: StatefulParser<A>,
second: StatefulParser<B>
): StatefulParser<B> {
return composeStateful(first, _ => second);
}
// Multi-digit number parser
const parseNumber: StatefulParser<number> =
statefulParser(state => {
let currentState = state;
let digits: number[] = [];
// Parse first digit (required)
const firstResult = digitStatefulParser.parse(currentState);
if (firstResult === null) return null;
digits.push(firstResult.result);
currentState = firstResult.newState;
// Parse additional digits (optional)
while (currentState.position < currentState.input.length) {
const digitResult = digitStatefulParser.parse(currentState);
if (digitResult === null) break;
digits.push(digitResult.result);
currentState = digitResult.newState;
}
// Convert digits array to number
const result = digits.reduce((acc, digit) => acc * 10 + digit, 0);
return { result, newState: currentState };
});
// Alternative combinator (try first, then second)
function alternative<A>(
first: StatefulParser<A>,
second: StatefulParser<A>
): StatefulParser<A> {
return statefulParser(state => {
const firstResult = first.parse(state);
if (firstResult !== null) return firstResult;
return second.parse(state);
});
}
// Parse identifier (letters followed by digits/letters)
const parseIdentifier: StatefulParser<string> =
statefulParser(state => {
let currentState = state;
let chars: string[] = [];
// First character must be a letter
if (currentState.position >= currentState.input.length) return null;
const firstChar = currentState.input[currentState.position];
if (!/[a-zA-Z]/.test(firstChar)) return null;
chars.push(firstChar);
currentState = { input: currentState.input, position: currentState.position + 1 };
// Subsequent characters can be letters or digits
while (currentState.position < currentState.input.length) {
const char = currentState.input[currentState.position];
if (!/[a-zA-Z0-9]/.test(char)) break;
chars.push(char);
currentState = { input: currentState.input, position: currentState.position + 1 };
}
return { result: chars.join(''), newState: currentState };
});
// Test functions
function testParsers() {
console.log("Testing simple parsers:");
console.log("parseFirstDigit('5abc'):", parseFirstDigit.parse("5abc")); // 5
console.log("charParser('a')('abc'):", charParser('a').parse("abc")); // 'a'
console.log("parseSpecificDigit('7')('7xyz'):", parseSpecificDigit('7').parse("7xyz")); // 7
console.log("\nTesting stateful parsers:");
const testState: StringState = { input: "123abc", position: 0 };
console.log("parseNumber('123abc'):", parseNumber.parse(testState)); // { result: 123, newState: { input: "123abc", position: 3 } }
const idState: StringState = { input: "hello123", position: 0 };
console.log("parseIdentifier('hello123'):", parseIdentifier.parse(idState)); // { result: "hello123", newState: { input: "hello123", position: 8 } }
// Test alternative parser with same types
const numberOrIdentifierLength = alternative(
parseNumber,
statefulParser(state => {
// Convert an identifier to its length so both branches return numbers
const idResult = parseIdentifier.parse(state);
if (idResult === null) return null;
return { result: idResult.result.length, newState: idResult.newState };
})
);
console.log("numberOrIdentifierLength('123abc'):", numberOrIdentifierLength.parse({ input: "123abc", position: 0 })); // 123
console.log("numberOrIdentifierLength('hello123'):", numberOrIdentifierLength.parse({ input: "hello123", position: 0 })); // 8
}
// Advanced: Fluent parser builder
class ParserBuilder<A> {
constructor(private _parser: StatefulParser<A>) {}
then<B>(next: StatefulParser<B>): ParserBuilder<B> {
return new ParserBuilder(thenStateful(this._parser, next));
}
map<B>(f: (a: A) => B): ParserBuilder<B> {
return new ParserBuilder(statefulParser(state => {
const result = this._parser.parse(state);
if (result === null) return null;
return { result: f(result.result), newState: result.newState };
}));
}
or(alternativeParser: StatefulParser<A>): ParserBuilder<A> {
return new ParserBuilder(alternative(this._parser, alternativeParser));
}
parse(input: string): A | null {
const result = this._parser.parse({ input, position: 0 });
return result ? result.result : null;
}
}
// Factory function for fluent interface
function parseWith<A>(parser: StatefulParser<A>): ParserBuilder<A> {
return new ParserBuilder(parser);
}
// Fluent interface example - corrected type handling
const numberParser = parseWith(parseNumber)
.map(n => n * 2);
const stringParser = parseWith(parseIdentifier)
.map(s => s.toUpperCase());
// For mixed types, we need to use union types
const mixedParser = parseWith(
alternative(
statefulParser(state => {
const result = parseNumber.parse(state);
return result ? { result: result.result as string | number, newState: result.newState } : null;
}),
statefulParser(state => {
const result = parseIdentifier.parse(state);
return result ? { result: result.result as string | number, newState: result.newState } : null;
})
)
);
console.log("Fluent parser test:");
console.log("numberParser('42abc'):", numberParser.parse("42abc")); // 84
console.log("stringParser('hello123'):", stringParser.parse("hello123")); // "HELLO123"
console.log("mixedParser('42abc'):", mixedParser.parse("42abc")); // 42
console.log("mixedParser('hello123'):", mixedParser.parse("hello123")); // "hello123"
// Run tests
testParsers();
C# can implement Arrow-like parser combinators using nullable types, generics, and LINQ-style syntax for structured parsing:
using System;
using System.Collections.Generic;
using System.Linq;
// Parser interface mimicking Arrow behavior for parsing
public interface IParser<TInput, TOutput>
where TOutput : class
{
TOutput? Parse(TInput input);
}
// Concrete parser implementation
public class Parser<TInput, TOutput> : IParser<TInput, TOutput>
where TOutput : class
{
private readonly Func<TInput, TOutput?> _parseFunc;
public Parser(Func<TInput, TOutput?> parseFunc)
{
_parseFunc = parseFunc;
}
public TOutput? Parse(TInput input) => _parseFunc(input);
}
// Static factory and composition methods
public static class Parser
{
// Helper to create parsers (similar to 'arr')
public static IParser<TInput, TOutput> From<TInput, TOutput>(
Func<TInput, TOutput?> parseFunc)
where TOutput : class
{
return new Parser<TInput, TOutput>(parseFunc);
}
// Parser composition operator (similar to '>>>')
public static IParser<TInput, TOutput3> Compose<TInput, TOutput1, TOutput3>(
IParser<TInput, TOutput1> first,
IParser<TOutput1, TOutput3> second)
where TOutput1 : class
where TOutput3 : class
{
return From<TInput, TOutput3>(input =>
{
var intermediate = first.Parse(input);
return intermediate != null ? second.Parse(intermediate) : default;
});
}
// Tuple wrapper class to satisfy reference type constraint
public class ParsedPair<T1, T2>
where T1 : class
where T2 : class
{
public T1 First { get; }
public T2 Second { get; }
public ParsedPair(T1 first, T2 second)
{
First = first;
Second = second;
}
public void Deconstruct(out T1 first, out T2 second)
{
first = First;
second = Second;
}
}
// Parallel parser composition for tuples
public static IParser<(TInput1, TInput2), ParsedPair<TOutput1, TOutput2>> Parallel<TInput1, TInput2, TOutput1, TOutput2>(
IParser<TInput1, TOutput1> left,
IParser<TInput2, TOutput2> right)
where TOutput1 : class
where TOutput2 : class
{
return From<(TInput1, TInput2), ParsedPair<TOutput1, TOutput2>>(inputs =>
{
var leftResult = left.Parse(inputs.Item1);
var rightResult = right.Parse(inputs.Item2);
return leftResult != null && rightResult != null
? new ParsedPair<TOutput1, TOutput2>(leftResult, rightResult)
: default;
});
}
// Alternative combinator (try first, then second)
public static IParser<TInput, TOutput> Alternative<TInput, TOutput>(
IParser<TInput, TOutput> first,
IParser<TInput, TOutput> second)
where TOutput : class
{
return From<TInput, TOutput>(input =>
{
var firstResult = first.Parse(input);
return firstResult ?? second.Parse(input);
});
}
}
// Extension methods for fluent interface
public static class ParserExtensions
{
public static IParser<TInput, TOutput3> Then<TInput, TOutput1, TOutput3>(
this IParser<TInput, TOutput1> first,
IParser<TOutput1, TOutput3> second)
where TOutput1 : class
where TOutput3 : class
{
return Parser.Compose(first, second);
}
public static IParser<TInput, TResult> Map<TInput, TOutput, TResult>(
this IParser<TInput, TOutput> parser,
Func<TOutput, TResult> mapper)
where TOutput : class
where TResult : class
{
return Parser.From<TInput, TResult>(input =>
{
var result = parser.Parse(input);
return result != null ? mapper(result) : default;
});
}
public static IParser<TInput, TOutput> Or<TInput, TOutput>(
this IParser<TInput, TOutput> first,
IParser<TInput, TOutput> second)
where TOutput : class
{
return Parser.Alternative(first, second);
}
}
// Basic parsing primitives
public static class BasicParsers
{
public static IParser<string, string> CharParser(char target) =>
Parser.From<string, string>(input =>
input.Length > 0 && input[0] == target ? target.ToString() : default);
public static IParser<string, string> DigitParser =>
Parser.From<string, string>(input =>
{
if (input.Length == 0) return default;
var c = input[0];
return char.IsDigit(c) ? (c - '0').ToString() : default;
});
public static IParser<string, string> ParseFirstDigit =>
Parser.From<string, string>(input =>
{
if (input.Length == 0) return default;
var firstChar = input[0];
return char.IsDigit(firstChar) ? (firstChar - '0').ToString() : default;
});
}
// Stateful parsing for position tracking
public struct ParseState
{
public string Input { get; }
public int Position { get; }
public ParseState(string input, int position = 0)
{
Input = input;
Position = position;
}
}
public struct ParseResult<T>
{
public T Result { get; }
public ParseState NewState { get; }
public ParseResult(T result, ParseState newState)
{
Result = result;
NewState = newState;
}
}
// Stateful parser interface
public interface IStatefulParser<T>
{
ParseResult<T>? Parse(ParseState state);
}
public class StatefulParser<T> : IStatefulParser<T>
{
private readonly Func<ParseState, ParseResult<T>?> _parseFunc;
public StatefulParser(Func<ParseState, ParseResult<T>?> parseFunc)
{
_parseFunc = parseFunc;
}
public ParseResult<T>? Parse(ParseState state) => _parseFunc(state);
}
// Stateful parser factory and combinators
public static class StatefulParser
{
// Wrapper classes for value types to satisfy reference type constraints
public class IntValue
{
public int Value { get; }
public IntValue(int value) => Value = value;
public static implicit operator int(IntValue wrapper) => wrapper.Value;
public static implicit operator IntValue(int value) => new IntValue(value);
public override string ToString() => Value.ToString();
}
public class CharValue
{
public char Value { get; }
public CharValue(char value) => Value = value;
public static implicit operator char(CharValue wrapper) => wrapper.Value;
public static implicit operator CharValue(char value) => new CharValue(value);
public override string ToString() => Value.ToString();
}
public static IStatefulParser<T> From<T>(Func<ParseState, ParseResult<T>?> parseFunc) =>
new StatefulParser<T>(parseFunc);
public static IStatefulParser<CharValue> CharParser(char target) =>
From<CharValue>(state =>
{
if (state.Position >= state.Input.Length) return null;
var currentChar = state.Input[state.Position];
if (currentChar != target) return null;
return new ParseResult<CharValue>(
new CharValue(currentChar),
new ParseState(state.Input, state.Position + 1)
);
});
public static IStatefulParser<IntValue> DigitParser =>
From<IntValue>(state =>
{
if (state.Position >= state.Input.Length) return null;
var currentChar = state.Input[state.Position];
if (!char.IsDigit(currentChar)) return null;
return new ParseResult<IntValue>(
new IntValue(currentChar - '0'),
new ParseState(state.Input, state.Position + 1)
);
});
public static IStatefulParser<IntValue> ParseNumber =>
From<IntValue>(state =>
{
var currentState = state;
var digits = new List<int>();
// Parse first digit (required)
var firstResult = DigitParser.Parse(currentState);
if (firstResult == null) return null;
digits.Add(firstResult.Value.Result.Value);
currentState = firstResult.Value.NewState;
// Parse additional digits (optional)
while (currentState.Position < currentState.Input.Length)
{
var digitResult = DigitParser.Parse(currentState);
if (digitResult == null) break;
digits.Add(digitResult.Value.Result.Value);
currentState = digitResult.Value.NewState;
}
// Convert digits to number
var result = digits.Aggregate(0, (acc, digit) => acc * 10 + digit);
return new ParseResult<IntValue>(new IntValue(result), currentState);
});
public static IStatefulParser<string> ParseIdentifier =>
From<string>(state =>
{
var currentState = state;
var chars = new List<char>();
// First character must be a letter
if (currentState.Position >= currentState.Input.Length) return null;
var firstChar = currentState.Input[currentState.Position];
if (!char.IsLetter(firstChar)) return null;
chars.Add(firstChar);
currentState = new ParseState(currentState.Input, currentState.Position + 1);
// Subsequent characters can be letters or digits
while (currentState.Position < currentState.Input.Length)
{
var currentChar = currentState.Input[currentState.Position];
if (!char.IsLetterOrDigit(currentChar)) break;
chars.Add(currentChar);
currentState = new ParseState(currentState.Input, currentState.Position + 1);
}
return new ParseResult<string>(new string(chars.ToArray()), currentState);
});
public static IStatefulParser<T> Alternative<T>(
IStatefulParser<T> first,
IStatefulParser<T> second) =>
From<T>(state =>
{
var firstResult = first.Parse(state);
return firstResult ?? second.Parse(state);
});
}
// Fluent parser builder for stateful parsers
public class StatefulParserBuilder<T>
where T : class
{
private readonly IStatefulParser<T> _parser;
public StatefulParserBuilder(IStatefulParser<T> parser)
{
_parser = parser;
}
public StatefulParserBuilder<TResult> Map<TResult>(Func<T, TResult> mapper)
where TResult : class =>
new StatefulParserBuilder<TResult>(
StatefulParser.From<TResult>(state =>
{
var result = _parser.Parse(state);
return result != null
? new ParseResult<TResult>(mapper(result.Value.Result), result.Value.NewState)
: null;
})
);
public StatefulParserBuilder<T> Or(IStatefulParser<T> alternative) =>
new StatefulParserBuilder<T>(StatefulParser.Alternative(_parser, alternative));
public T? Parse(string input)
{
var result = _parser.Parse(new ParseState(input));
return result?.Result;
}
}
// Factory function for fluent interface
public static class StatefulParserBuilder
{
public static StatefulParserBuilder<T> From<T>(IStatefulParser<T> parser)
where T : class =>
new StatefulParserBuilder<T>(parser);
}
// Example usage and tests
public class ParserCombinatorExample
{
public static void Main()
{
TestBasicParsers();
TestStatefulParsers();
TestFluentParsers();
}
private static void TestBasicParsers()
{
Console.WriteLine("Testing basic parsers:");
var charParserA = BasicParsers.CharParser('a');
Console.WriteLine($"CharParser('a').Parse(\"abc\"): {charParserA.Parse("abc")}"); // "a"
var parseFirstDigit = BasicParsers.ParseFirstDigit;
Console.WriteLine($"ParseFirstDigit.Parse(\"5abc\"): {parseFirstDigit.Parse("5abc")}"); // "5"
var parseSpecificDigit = BasicParsers.CharParser('7').Then(BasicParsers.DigitParser);
Console.WriteLine($"ParseSpecificDigit.Parse(\"7\"): {parseSpecificDigit.Parse("7")}"); // "7"
}
private static void TestStatefulParsers()
{
Console.WriteLine("\nTesting stateful parsers:");
var testState = new ParseState("123abc");
var numberResult = StatefulParser.ParseNumber.Parse(testState);
Console.WriteLine($"ParseNumber(\"123abc\"): Result={numberResult?.Result.Value}, Position={numberResult?.NewState.Position}");
var idState = new ParseState("hello123");
var idResult = StatefulParser.ParseIdentifier.Parse(idState);
Console.WriteLine($"ParseIdentifier(\"hello123\"): Result={idResult?.Result}, Position={idResult?.NewState.Position}");
// Test alternative parser with same types
var numberOrIdentifierLength = StatefulParser.Alternative(
StatefulParser.ParseNumber,
StatefulParser.From<StatefulParser.IntValue>(state =>
{
var idResult = StatefulParser.ParseIdentifier.Parse(state);
if (idResult == null) return null;
return new ParseResult<StatefulParser.IntValue>(
new StatefulParser.IntValue(idResult.Value.Result.Length),
idResult.Value.NewState);
})
);
Console.WriteLine($"NumberOrIdentifierLength(\"123abc\"): {numberOrIdentifierLength.Parse(new ParseState("123abc"))?.Result.Value}"); // 123
Console.WriteLine($"NumberOrIdentifierLength(\"hello123\"): {numberOrIdentifierLength.Parse(new ParseState("hello123"))?.Result.Value}"); // 8
}
private static void TestFluentParsers()
{
Console.WriteLine("\nTesting fluent parsers:");
var numberParser = StatefulParserBuilder.From(StatefulParser.ParseNumber)
.Map(n => new StatefulParser.IntValue(n.Value * 2));
Console.WriteLine($"NumberParser(\"42abc\"): {numberParser.Parse("42abc")?.Value}"); // 84
var stringParser = StatefulParserBuilder.From(StatefulParser.ParseIdentifier)
.Map(s => s.ToUpper());
Console.WriteLine($"StringParser(\"hello123\"): {stringParser.Parse("hello123")}"); // HELLO123
var combinedParser = StatefulParserBuilder.From(StatefulParser.ParseNumber)
.Map(n => n.Value.ToString())
.Or(StatefulParser.ParseIdentifier);
Console.WriteLine($"CombinedParser(\"42abc\"): {combinedParser.Parse("42abc")}"); // "42"
Console.WriteLine($"CombinedParser(\"hello123\"): {combinedParser.Parse("hello123")}"); // "hello123"
}
}
State Machine with Arrows #
Arrows naturally model state machines with clear state transitions:
{-# LANGUAGE Arrows #-}
import Control.Arrow
import Control.Category
import Prelude hiding (id, (.))
-- State machine Arrow
newtype StateMachine s a b = StateMachine
{ runStateMachine :: s -> a -> (b, s) }
instance Category (StateMachine s) where
id = StateMachine $ \s a -> (a, s)
StateMachine f . StateMachine g = StateMachine $ \s a ->
let (b, s') = g s a
(c, s'') = f s' b
in (c, s'')
instance Arrow (StateMachine s) where
arr f = StateMachine $ \s a -> (f a, s)
first (StateMachine f) = StateMachine $ \s (a, c) ->
let (b, s') = f s a
in ((b, c), s')
instance ArrowChoice (StateMachine s) where
left (StateMachine f) = StateMachine $ \s input -> case input of
Left a -> let (b, s') = f s a in (Left b, s')
Right c -> (Right c, s)
right (StateMachine f) = StateMachine $ \s input -> case input of
Left a -> (Left a, s)
Right c -> let (b, s') = f s c in (Right b, s')
-- State machine primitives
getState :: StateMachine s a s
getState = StateMachine $ \s _ -> (s, s)
setState :: StateMachine s s ()
setState = StateMachine $ \_ s -> ((), s)
modifyState :: (s -> s) -> StateMachine s a ()
modifyState f = StateMachine $ \s _ -> ((), f s)
-- Traffic light state machine
data LightState = Red | Yellow | Green deriving (Show, Eq)
data LightInput = TimerTick | EmergencyStop deriving (Show, Eq)
trafficLight :: StateMachine LightState LightInput LightState
trafficLight = proc input -> do
currentState <- getState -< ()
newState <- arr transition -< (currentState, input)
setState -< newState
returnA -< newState
where
transition (Red, TimerTick) = Green
transition (Green, TimerTick) = Yellow
transition (Yellow, TimerTick) = Red
transition (_, EmergencyStop) = Red
-- Counter with reset functionality
counterMachine :: StateMachine Int String Int
counterMachine = proc command -> do
count <- getState -< ()
case command of
"inc" -> do
modifyState (+1) -< ()
getState -< ()
"dec" -> do
modifyState (subtract 1) -< ()
getState -< ()
"reset" -> do
setState -< 0
returnA -< 0
_ -> returnA -< count
-- Working example functions
main :: IO ()
main = do
putStrLn "Testing counter machine:"
print $ runStateMachine counterMachine 0 "inc" -- (1, 1)
print $ runStateMachine counterMachine 5 "dec" -- (4, 4)
print $ runStateMachine counterMachine 10 "reset" -- (0, 0)
putStrLn "Testing traffic light:"
print $ runStateMachine trafficLight Red TimerTick -- (Green, Green)
print $ runStateMachine trafficLight Green TimerTick -- (Yellow, Yellow)
print $ runStateMachine trafficLight Yellow EmergencyStop -- (Red, Red)
-- Example usage:
-- runStateMachine counterMachine 0 "inc" evaluates to (1, 1)
-- runStateMachine trafficLight Red TimerTick evaluates to (Green, Green)
// State machine type mimicking Arrow behavior
type StateMachine<S, A, B> = {
run: (state: S, input: A) => { result: B; newState: S };
};
// Helper to create state machines
function stateMachine<S, A, B>(f: (state: S, input: A) => { result: B; newState: S }): StateMachine<S, A, B> {
return { run: f };
}
// Identity state machine
function idStateMachine<S, A>(): StateMachine<S, A, A> {
return stateMachine((state, input) => ({ result: input, newState: state }));
}
// Composition
function composeStateMachine<S, A, B, C>(
first: StateMachine<S, A, B>,
second: StateMachine<S, B, C>
): StateMachine<S, A, C> {
return stateMachine((state, input) => {
const { result: b, newState: s1 } = first.run(state, input);
const { result: c, newState: s2 } = second.run(s1, b);
return { result: c, newState: s2 };
});
}
// Arrow-like combinators
function arrStateMachine<S, A, B>(f: (a: A) => B): StateMachine<S, A, B> {
return stateMachine((state, input) => ({ result: f(input), newState: state }));
}
function firstStateMachine<S, A, B, C>(
sm: StateMachine<S, A, B>
): StateMachine<S, [A, C], [B, C]> {
return stateMachine((state, [a, c]) => {
const { result: b, newState: s1 } = sm.run(state, a);
return { result: [b, c], newState: s1 };
});
}
// State machine primitives
function getState<S, A>(): StateMachine<S, A, S> {
return stateMachine((state, _) => ({ result: state, newState: state }));
}
function setState<S>(): StateMachine<S, S, void> {
return stateMachine((_, newState) => ({ result: undefined, newState }));
}
function modifyState<S, A>(f: (s: S) => S): StateMachine<S, A, void> {
return stateMachine((state, _) => ({ result: undefined, newState: f(state) }));
}
// Traffic light state machine
type LightState = "Red" | "Yellow" | "Green";
type LightInput = "TimerTick" | "EmergencyStop";
function transition([state, input]: [LightState, LightInput]): LightState {
if (input === "EmergencyStop") return "Red";
if (state === "Red" && input === "TimerTick") return "Green";
if (state === "Green" && input === "TimerTick") return "Yellow";
if (state === "Yellow" && input === "TimerTick") return "Red";
return state;
}
const trafficLight: StateMachine<LightState, LightInput, LightState> = stateMachine((state, input) => {
const newState = transition([state, input]);
return { result: newState, newState };
});
// Counter with reset functionality
const counterMachine: StateMachine<number, string, number> = stateMachine((state, command) => {
let newState = state;
if (command === "inc") newState += 1;
else if (command === "dec") newState -= 1;
else if (command === "reset") newState = 0;
return { result: newState, newState };
});
// Example usage
function testStateMachines() {
// Counter
console.log("Counter:");
let s = 0;
let r = counterMachine.run(s, "inc"); console.log(r); s = r.newState;
r = counterMachine.run(s, "dec"); console.log(r); s = r.newState;
r = counterMachine.run(s, "reset"); console.log(r);
// Traffic light
console.log("\nTraffic Light:");
let t: LightState = "Red";
let tl = trafficLight.run(t, "TimerTick"); console.log(tl); t = tl.newState;
tl = trafficLight.run(t, "TimerTick"); console.log(tl); t = tl.newState;
tl = trafficLight.run(t, "EmergencyStop"); console.log(tl);
}
testStateMachines();
C# can model Arrow-like state machines using generic classes and composition.
// State machine Arrow
public class StateMachine<S, A, B>
{
private readonly Func<S, A, (B, S)> run;
public StateMachine(Func<S, A, (B, S)> run) { this.run = run; }
public (B, S) Run(S state, A input) => run(state, input);
// Identity
public static StateMachine<S, A, A> Id() => new StateMachine<S, A, A>((s, a) => (a, s));
// Composition
public StateMachine<S, A, C> Then<C>(StateMachine<S, B, C> next) where C : notnull
=> new StateMachine<S, A, C>((s, a) => {
var (b, s1) = this.Run(s, a);
var (c, s2) = next.Run(s1, b);
return (c, s2);
});
// Arrow combinator: arr
public static StateMachine<S, A, B> Arr(Func<A, B> f)
=> new StateMachine<S, A, B>((s, a) => (f(a), s));
// Arrow combinator: first
public static StateMachine<S, (A, C), (B, C)> First<C>(StateMachine<S, A, B> sm) where C : notnull
=> new StateMachine<S, (A, C), (B, C)>((s, ac) => {
var (a, c) = ac;
var (b, s1) = sm.Run(s, a);
return ((b, c), s1);
});
}
// State machine primitives
public static class StateMachinePrimitives
{
public static StateMachine<S, A, S> GetState<S, A>()
=> new StateMachine<S, A, S>((s, _) => (s, s));
public static StateMachine<S, S, Unit> SetState<S>()
=> new StateMachine<S, S, Unit>((_, s) => (new Unit(), s));
public static StateMachine<S, A, Unit> ModifyState<S, A>(Func<S, S> f)
=> new StateMachine<S, A, Unit>((s, _) => (new Unit(), f(s)));
public struct Unit { }
}
// Traffic light state machine
public enum LightState { Red, Yellow, Green }
public enum LightInput { TimerTick, EmergencyStop }
public static class TrafficLightMachine
{
public static LightState Transition(LightState state, LightInput input)
{
if (input == LightInput.EmergencyStop) return LightState.Red;
if (state == LightState.Red && input == LightInput.TimerTick) return LightState.Green;
if (state == LightState.Green && input == LightInput.TimerTick) return LightState.Yellow;
if (state == LightState.Yellow && input == LightInput.TimerTick) return LightState.Red;
return state;
}
public static StateMachine<LightState, LightInput, LightState> Machine =
new StateMachine<LightState, LightInput, LightState>((state, input) => {
var newState = Transition(state, input);
return (newState, newState);
});
}
// Counter with reset functionality
public static class CounterMachine
{
public static StateMachine<int, string, int> Machine =
new StateMachine<int, string, int>((state, command) => {
int newState = state;
if (command == "inc") newState++;
else if (command == "dec") newState--;
else if (command == "reset") newState = 0;
return (newState, newState);
});
}
// Example usage
public static class Program
{
public static void Main()
{
// Counter
Console.WriteLine("Counter:");
int s = 0;
var r = CounterMachine.Machine.Run(s, "inc"); Console.WriteLine(r); s = r.Item2;
r = CounterMachine.Machine.Run(s, "dec"); Console.WriteLine(r); s = r.Item2;
r = CounterMachine.Machine.Run(s, "reset"); Console.WriteLine(r);
// Traffic light
Console.WriteLine("\nTraffic Light:");
LightState t = LightState.Red;
var tl = TrafficLightMachine.Machine.Run(t, LightInput.TimerTick); Console.WriteLine(tl); t = tl.Item2;
tl = TrafficLightMachine.Machine.Run(t, LightInput.TimerTick); Console.WriteLine(tl); t = tl.Item2;
tl = TrafficLightMachine.Machine.Run(t, LightInput.EmergencyStop); Console.WriteLine(tl);
}
}
Visualizing Arrow #
1. Basic Arrow Structure
Input ---> [Arrow f] ---> Output
A B
An Arrow `f` transforms input of type `A` to output of type `B`.
2. Arrow Composition (`>>>`)
A ---> [f] ---> B ---> [g] ---> C
Equivalent to:
A ---> [f >>> g] ---> C
Sequential composition: the output of `f` becomes the input of `g`.
3. Product Operations
3.a First (`first f`)
┌─── A ---> [f] ---> B ───┐
│ │
(A,C)┤ ├─(B,C)
│ │
└─── C ──────────── C ────┘
The Arrow `f` operates on the first component, leaving the second unchanged.
3.b Second (`second g`)
┌─── A ─────────────── A ───┐
│ │
(A,C)┤ ├─(A,D)
│ │
└─── C --> [g] --> D ───────┘
The Arrow `g` operates on the second component, leaving the first unchanged.
4. Paired Composition (`f *** g`)
┌─── A ---> [f] ---> B ───┐
│ │
(A,C)┤ ├─(B,D)
│ │
└─── C ---> [g] ---> D ───┘
Each Arrow operates on its respective component. The combinator expresses paired routing, not necessarily simultaneous or parallel execution; effects may be sequenced.
5. Arrow Laws Visualization
5.a Identity Law
A ---> [id] ---> A (Identity does nothing)
A ---> [f >>> id] ---> B ≡ A ---> [f] ---> B
5.b Associativity Law
A ---> [f] ---> B ---> [g] ---> C ---> [h] ---> D
Both groupings are equivalent:
A ---> [(f >>> g) >>> h] ---> D
A ---> [f >>> (g >>> h)] ---> D
6. First/Second Laws
first id ≡ id:
┌─── A ─────── A ───┐ ┌─── A ─────── A ───┐
│ │ │ │
(A,C)┤ ├─(A,C) ≡ (A,C)┤ ├─(A,C)
│ │ │ │
└─── C ─────── C ───┘ └─── C ─────── C ───┘
first id id
first (f >>> g) ≡ first f >>> first g:
┌─── A ---> [f] ---> B ---> [g] ---> C ───┐
│ │
(A,D)┤ ├─(C,D)
│ │
└─────── D ────────────────── D ──────────┘
first (f >>> g)
≡
┌─── A ---> [f] ---> B ───┐ ┌─── B ---> [g] ---> C ───┐
│ │ │ │
(A,D)┤ ├─(B,D)┤ ├─(C,D)
│ │ │ │
└───── D ────────── D ────┘ └───── D ────────── D ────┘
first f first g
7. Complex Arrow Network
┌─── [f₁] ---> B₁ ───┐
│ │
┌─── A₁ ───┤ │---> [combine] ---> D
│ │ │
Input ───┤ └─── [f₂] ---> B₂ ───┘
│
└─── A₂ ------> [g] ----------------> C---> [final] ---> Output
Multi-path Arrow computation with fan-out and fan-in
8. Profunctor View
Pure input map Pure output map
X' -------- u --------> X Y -------- v --------> Y'
│ │
└── p : A(X,Y) ┘
arr u >>> p >>> arr v
│
▼
A(X',Y')
The input position is contravariant; the output position is covariant:
A : Cᵒᵖ × C ---> Set
(X,Y) |---> A(X,Y)
9. Freyd Category: Pure and Effectful Computations
pure category C
X -------- f --------> Y
│ │
│ same objects │ same objects
▼ ▼
X ====== arr f ======> Y
X ======== p =========> Y
computation category D
J : C ---> D
J(f) = arr f
`C` contains pure morphisms. `D` also contains general Arrow
computations such as `p`. The functor `J` embeds the pure ones.
10. Premonoidal Ordering
For p : A -> B and q : C -> D, two orders are possible:
(A,C) -- first p --> (B,C) -- second q --> (B,D)
(A,C) -- second q -> (A,D) -- first p --> (B,D)
In general:
first p >>> second q ≠ second q >>> first p
The tensor is therefore premonoidal: each side is functorial on its
own, but independent effectful computations need not commute.
11. Central Pure Morphisms
When h is pure, `arr h` is central, so it commutes with an independent
effect q:
first (arr h) >>> second q = second q >>> first (arr h)
(A,C) -- first (arr h) --> (B,C) -- second q ------> (B,D)
║ ║
╚══ second q =========> (A,D) == first (arr h) =========╝
The upper path applies the pure map first; the lower path applies the
independent effect first. Centrality says that the results agree.
12. Two Equivalent Categorical Views
Monoid in internally strong profunctors Freyd category
A : Cᵒᵖ × C -> Set J : C -> D
arr = unit pure embedding
(>>>) = multiplication composition in D
first = internal strength premonoidal structure
equivalent
under the standard assumptions
Conclusion #
Arrows offer an insight into the nature of computation: not every problem needs value-dependent monadic sequencing, while many problems need more structure than plain function composition provides. Arrows trade some forms of dynamic composition for an explicit input-output interface.
What makes basic Arrows useful is not a capability unavailable to monads, but the structure exposed by restricting value-dependent composition.
🔍 Static structure: Some Arrow representations retain an analyzable shape before execution
- Reified circuits can be rendered as diagrams
- Reified parser structure can support analysis or optimization
- Domain-specific representations can track resource requirements
⚡ Optimization opportunities: A suitable representation can enable transformations
- Independent circuit stages may be candidates for parallel execution
- Parser descriptions may be transformed into efficient state machines
- Explicit resource information may support ahead-of-time optimization
🧩 Compositional clarity: The Arrow laws ensure coherent behavior
f >>> (g >>> h) = (f >>> g) >>> h— composition is associativefirst f >>> first g = first (f >>> g)— structure is preservedarr id = id— pure functions lift cleanly
Arrows shine in domains where:
- Structure Matters: When the shape of computation is as important as the result
- Static Analysis: When you need to examine computations without running them
- Resource Management: When computational resources need careful coordination
- Domain Modeling: When your problem naturally fits Arrow patterns
Avoid Arrows when you need:
- Dynamic selection of computations from intermediate results (use a monad)
- Simple independent contextual computations (use Applicative)
- Pure transformations without computational context (use ordinary functions or Functor as appropriate)
As functional programming matures, Arrows represent an important lesson: constraint enables capability. By voluntarily limiting our computational power, we gain new abilities.
The Arrow way: Embrace structure, gain insight, and discover that sometimes the most powerful abstraction is the one that knows its limits.
Source code #
Reference implementation (opens in a new tab)
References
- Product (category theory) (opens in a new tab) · Back
- Monoidal functor (opens in a new tab) · Back
- Monoidal Categories (opens in a new tab) · Back
- Lax Monoidal Functors (opens in a new tab) · Back
- Strong Monoidal Functors (opens in a new tab) · Back
- Arrow tutorial (opens in a new tab) · Back
- Arrow (opens in a new tab) · Back
- Programming with arrows (opens in a new tab) · Back
- Arrows, like Monads, are Monoids (opens in a new tab) · Back
- Control.Arrow (opens in a new tab) · Back
- Categorical Semantics for Arrows (opens in a new tab) · Back