Arrow category

Meta-programming, code as data

Publish at:
    _    ____  ____   _____        __
   / \  |  _ \|  _ \ / _ \ \      / /
  / _ \ | |_) | |_) | | | \ \ /\ / /
 / ___ \|  _ <|  _ <| |_| |\ V  V /
/_/___\_\_| \_\_|_\_\\___/__\_/\_/  ____  ___ _____ ____
 / ___|  / \|_   _| ____/ ___|/ _ \|  _ \|_ _| ____/ ___|
| |     / _ \ | | |  _|| |  _| | | | |_) || ||  _| \___ \
| |___ / ___ \| | | |__| |_| | |_| |  _ < | || |___ ___) |
 \____/_/   \_\_| |_____\____|\___/|_| \_\___|_____|____/

It might be a bit mind-bending, but functions themselves form a category. It has several names: arrow category[1], category of morphisms (and it can be described as the functor category C^[1][2]). From a categorical point of view, for something to be a category, it needs objects, morphisms and some additional properties. If we take an arbitrary category and inspect its morphisms, we can infer that a new category can be constructed, where

  • Objects are morphisms (arrows) in the original category.
  • Morphisms between two arrows f:A -> B and g:C -> D are pairs of morphisms s:A -> C, t:B -> D such that the following square applies transformations in a commutative manner[3]:
A ---f---> B
|          |
s          t
|          |
▼          ▼
C ---g---->D

OK, objects are clear, morphisms - not so much. And what is this commutative thing again? Proving that this category is in fact a category is not that difficult. As an exercise, it illustrates functional thinking at its core. Enough time wasted.

We start with the initial category C, proving that arrow category Arr(C) is category.

For Arr(C) we have:

  • Objects are morphisms (arrows) f:A -> B in C
  • Morphisms are pairs of morphisms (s: A -> C, t: B -> D) in C so, the application of the arrows is commutative (gives the same result whatever the order of the quantities involved). That means, given the square
A ---f---> B
|          |
s          t
|          |
▼          ▼
C ---g---->D

For any morphism (s, t): f -> g in Arr(C), t ∘ f = g ∘ s.

Composition in arrow category #

Visual proof first. If we take two morphisms from the original category, say f and g.

A ---f---> B

C ---g---> D

What we need is a glue that transforms one into another. And that glue is two arrows that connect objects from the top to the objects from the bottom, respectively.

A    B
|    |
s    t
|    |
▼    ▼
C    D

But not in any arbitrary way. Application of g and s consecutively has to be the same as the application of the t and f.

  • g ∘ s = t ∘ f
  • g(s(x)) == t(f(x))

So, the application of the arrows is commutative, and the whole square is called a commutative square.

The final morphism α:f -> g is the one we are interested. Technically it's a morphism (s, t): f -> g that commutes.

  A --f-->B
  |       |
  g       h
  |       |
  ▼       ▼
  A'--f'->B'

Here f: A -> B and f': A' -> B' are objects in our new category, and g: A -> A' and h: B -> B' are morphisms in the original category.

And we can also compose these new types of morphisms. If we put two of them side by side:

  A --f--> B --g-->C
  |        |       |
  k        l       m
  |        |       |
  ▼        ▼       ▼
  A'--f'-->B'--g'->C'

We can get to C' via two ways. From A to A' to B' to C', and from A to B to C to C'. More formally: g'(f'(k(x))) == m(g(f(x))), or g' ∘ f' ∘ k == m ∘ g ∘ f.

The composition is also associative because all the morphisms from original category are associative.

Another way of proving composition is just by following the path of morphisms.

Suppose we have three objects (arrows) in C:

  • f: A -> B
  • g: C -> D
  • h: E -> F

Suppose we have morphisms:

  • (s, t): f -> g where s: A -> C, t: B -> D, and t ∘ f = g ∘ s
  • (s',t'): g -> h where s': C -> E, t': D -> F, and t' ∘ g = h ∘ s'

Composition of morphisms means:

  • (s', t') ∘ (s, t) = (s' ∘ s, t' ∘ t)

The morphism from f to h is valid because (t' ∘ t) ∘ f = h ∘ (s' ∘ s).

  • Left side composition: (t' ∘ t) ∘ f = t' ∘ (t ∘ f). From the first square t ∘ f = g ∘ s, so t' ∘ (g ∘ s) = (t' ∘ g) ∘ s. From the second square t' ∘ g = h ∘ s', so (h ∘ s') ∘ s = h ∘ (s' ∘ s)

Associativity #

Suppose we have three composable morphisms

  • (s, t): f -> g
  • (s', t'): g -> h
  • (s'', t''): h -> k

Then

  • ((s'', t'') ∘ (s', t')) ∘ (s, t) = (s'' ∘ s', t'' ∘ t') ∘ (s, t) = ((s'' ∘ s') ∘ s, (t'' ∘ t') ∘ t)
  • (s'', t'') ∘ ((s', t') ∘ (s, t)) = (s'', t'') ∘ (s' ∘ s, t' ∘ t) = (s'' ∘ (s' ∘ s), t'' ∘ (t' ∘ t))

And composition of C is associative by definition

(s'' ∘ s') ∘ s = s'' ∘ (s' ∘ s) and (t'' ∘ t') ∘ t = t'' ∘ (t' ∘ t)

Therefore, composition in Arr(C) is associative.

Identity in arrow category #

Suppose we have a morphism (s, t): f -> g in Arr(C) and id as identity morphism in C.

  • Left identity: (id(C), id(D)) ∘ (s, t) = (id(C) ∘ s, id(D) ∘ t) = (s, t)
  • Right identity: (s, t) ∘ (id(A), id(B)) = (s ∘ id(A), t ∘ id(B)) = (s, t)

Both statements are valid because identities in C are identities for composition.

Or using commutative diagram

Given three original morphisms f:A -> B, Id:A -> A and ID:B -> B, the following diagram commutes.

  A ---f---> B
  |         |
 ID(A)     ID(B)
  |         |
  ▼         ▼
  A ---f---> B

ID(B)(f(x)) == f(ID(A) (x)) or ID(B) ∘ f == f ∘ ID(A). Hence we get a valid morphism that is denoted as a pair (ID(A), ID(B)). This pair is actually an identity for two reasons:

  • (ID(A), ID(B)) ∘ (h, k) == (ID(A) ∘ h, ID(B) ∘ k) - left identity
  • (h, k) ∘ (ID(A), ID(B)) == (h ∘ ID(A), k ∘ ID(B)) - right identity

Functor category #

The arrow category Arr(C) is (isomorphic to) the functor category C^[1], where [1] is the walking arrow category:

It has two objects 0, 1 and exactly one non-identity morphism u: 0 -> 1 (plus id_0, id_1). A functor F: [1] -> C picks out an arrow in C (namely F(u): F(0) -> F(1)), so functors [1] -> C correspond to arrows of C. A natural transformation between two such functors is exactly a commutative square in C, which is why morphisms in Arr(C) are commutative squares.

Arrow example #

And this is how it looks in code:

Key Insights #

The Arrow Category construction reveals profound insights about the nature of functions and categorical structure:

  1. Functions as Objects: The fundamental insight is that morphisms (functions) in one category can become objects in another category. This demonstrates the categorical principle that "everything can be objectified" - structure at one level becomes data at the next level.

  2. Commutative Squares as Morphisms: The morphisms in Arr(C) are not simple functions but pairs of functions (s, t) that must satisfy the commutativity condition t ∘ f = g ∘ s. This means:

    • Morphisms encode relationships between functions
    • The commutative square ensures consistency across different "paths" of computation
  3. Dual Nature of Functions: In the arrow category, functions have a dual nature:

    • As objects: They can be transformed, compared, and manipulated
    • As structure: They still encode computational relationships This duality is fundamental to higher-order programming and metaprogramming.
  4. Compositional Structure: The composition in Arr(C) is defined as (s', t') ∘ (s, t) = (s' ∘ s, t' ∘ t), which means:

    • Composition preserves the commutative property
    • The arrow category inherits associativity from the base category
    • Identity morphisms are pairs of identities: (id_A, id_B)
  5. Universal Construction: Arrow categories are a universal way to "lift" any category to a higher level where:

    • Morphisms become first-class citizens
    • Relationships between morphisms can be studied systematically
  6. Programming Implications: This construction underlies several programming concepts:

    • Higher-order functions: Functions that operate on other functions
    • Function transformers: Function decorators, and middleware
    • Program analysis: Static analysis tools that examine relationships between functions
    • Refactoring: Systematic transformations that preserve program behavior

Understanding arrow categories helps programmers:

  • Design better abstractions for function manipulation
  • Understand the theoretical foundations of functional programming
  • Recognize patterns in code transformation and optimization
  • Build more composable and modular systems

Applicability #

Arrow categories are useful because they let us reason about morphisms themselves and about commutative squares between them. That point of view appears in several areas of programming, especially when we compare or transform programs in a structure-preserving way.

It is important to separate this from Haskell's Arrow typeclass. They share the word "arrow", but they are not the same construction:

  • Arr(C) is the category of morphisms in a category C
  • Haskell Arrow is a programming abstraction for generalized computations with operations such as arr and first

The Haskell abstraction is still a useful related example of structured composition, so the code below should be read as a programming analogue, not as a direct implementation of the categorical arrow category.

TypeScript doesn't have built-in Arrow support, but we can implement the Arrow pattern to demonstrate the concept of composable computations with structure preservation.

C# doesn't have native Arrow support, but we can implement the Arrow pattern using interfaces and classes to demonstrate composable computations.

Visualizing arrow categories #

0. Walking arrow category [1]

[1] has two objects (0 and 1) and exactly one non-identity morphism
u: 0 → 1 (plus identities).

0 ----u----> 1
|           |
id_0        id_1
|           |
▼           ▼
0           1

Composition laws:
  u ∘ id_0 = u
  id_1 ∘ u = u

1. Basic Structure

Original Category C:              Arrow Category Arr(C):

A ----f----> B                   [f: A→B] --------> [g: C→D]
             |                      ^                 ^
             h                      |                 |
             |                   Objects          Objects
             ▼                   (arrows           (arrows
C ----g----> D                   from C)          from C)


2. Morphisms as Commutative Squares

A morphism α: f → g in Arr(C) is a pair (s: A → C, t: B → D) such that the square commutes:

Morphism α = (s, t): f → g

A ----f----> B
|            |
s     α      t     ← This entire square is the morphism α
|            |
▼            ▼
C ----g----> D

Commutativity condition: t ∘ f = g ∘ s

Step-by-step:

Step 1: Start with two arrows f and g
   A ----f----> B     C ----g----> D

Step 2: Find connecting morphisms s and t
   A            B
   |            |
   s            t
   |            |
   ▼            ▼
   C            D

Step 3: Verify commutativity
   A ----f----> B
   |            |
   s    CHECK   t    ← Does t∘f = g∘s ?
   |            |
   ▼            ▼
   C ----g----> D


3. Composition of Morphisms

Given two morphisms α: f → g and β: g → h, their composition β ∘ α: f → h is visualized as:

First morphism α = (s₁, t₁): f → g

A ----f----> B
|            |
s₁           t₁
|            |
▼            ▼
C ----g----> D

Second morphism β = (s₂, t₂): g → h

C ----g----> D
|            |
s₂           t₂
|            |
▼            ▼
E ----h----> F

Composition β ∘ α = (s₂ ∘ s₁, t₂ ∘ t₁): f → h

A ----f----> B
|            |
s₂∘s₁       t₂∘t₁
|            |
▼            ▼
E ----h----> F

Extended composition diagram:

A ----f----> B
|            |
s₁           t₁
|            |
▼            ▼
C ----g----> D
|            |
s₂           t₂
|            |
▼            ▼
E ----h----> F

Final composed morphism: (s₂∘s₁, t₂∘t₁): f → h


4. Identity Morphisms

For any arrow f: A → B, the identity morphism id_f is the pair (id_A, id_B):

Identity morphism id_f = (id_A, id_B): f → f

A ----f----> B
|            |
id_A    ✓    id_B   ← Trivially commutes
|            |
▼            ▼
A ----f----> B

Verification: id_B ∘ f = f = f ∘ id_A ✓

5. Composition Requires Chosen Squares

Having composable arrows in the original category

f: A → B,  g: B → C,  h: C → D

does not by itself produce morphisms `f → g` and `g → h` in `Arr(C)`.
To get those, we must choose commutative squares. For example:

Morphism α = (s₁, t₁): f → g

A ----f----> B
|            |
s₁           t₁
|            |
▼            ▼
B ----g----> C

with `t₁ ∘ f = g ∘ s₁`, and morphism β = (s₂, t₂): g → h

B ----g----> C
|            |
s₂           t₂
|            |
▼            ▼
C ----h----> D

with `t₂ ∘ g = h ∘ s₂`.

Only after choosing such squares do we get the composite

β ∘ α = (s₂ ∘ s₁, t₂ ∘ t₁): f → h

A ----f----> B
|            |
s₂∘s₁       t₂∘t₁
|            |
▼            ▼
C ----h----> D

6. Programming Analogy

Original Category (Types and Functions):
String ----length----> Int ----square----> Int

Arrow Category (Function Transformations):
[length] -----------------> [square]
   ↑                           ↑
   |                           |
Objects are                Objects are
functions                  functions

Morphism between them:
String ----length----> Int
  |                     |
  length             square    ← (length, square): length → square
  |                     |
  ▼                     ▼
Int   ----square----> Int

Conclusion #

The Arrow pattern provides:

  • Composability: Arrows can be systematically composed to build complex computations from simple parts
  • Structure Preservation: The first and second operations preserve the structure of product types
  • Error Handling: Maybe/Option arrows handle null values gracefully
  • Logging/Tracing: Logging arrows add observability without changing computation logic
  • State Management: State arrows thread state through computations
  • Modularity: Each arrow encapsulates a specific aspect of computation
  • Reusability: Arrows can be reused and recombined in different contexts

This programming abstraction is particularly useful for building robust, composable systems where computations need to be combined while preserving their essential structure and properties.

Separately, the categorical arrow category Arr(C) remains the construction introduced earlier in this article: objects are morphisms of C, and morphisms are commutative squares.

Source code #

Reference implementation (opens in a new tab)

References

  1. Arrow categories (opens in a new tab) · Back
  2. Functor category (Wikipedia) (opens in a new tab) · Back
  3. Commutative diagram (Wikipedia) (opens in a new tab) · Back