Asymmetry — Why Products and Coproducts Feel So Different
Category theory often promises symmetry. But real systems remind us: **direction changes everything**.
We saw something beautiful:
Mathematically, they are dual.
But then something surprising happens:
In the category of sets… they behave *very differently*.
Why?
Because functions are asymmetric.
Duality vs Reality
Category theory gives us a beautiful and powerful idea:
Reverse arrows → get a dual conceptThat’s why we get pairs like:
From a theoretical point of view, these are perfect duals — one is obtained from the other just by reversing the direction of arrows.
But what happens in the real world (Set)?
When we move from abstract theory to actual sets, something interesting appears:
|A × B| = |A| × |B|
|A + B| = |A| + |B|Now the difference becomes concrete:
Why this happens
A product combines values:
You get all possible combinations
A coproduct represents a choice:
you take a value from A or a value from B but never both
You only get one side at a time
Even though product and coproduct are dual in theory:
They represent fundamentally different ideas in practice.
Where duality “breaks”
Duality is about structure (arrows), but real behavior comes from values and functions.
Since functions:
Duality gives us symmetry in theory, but in reality, combining and choosing are fundamentally different operations.
Initial vs Terminal — Where Asymmetry First Appears
One of the earliest places where symmetry breaks is in the behavior of initial and terminal objects.
In theory, they are duals. In reality, they behave very differently.
Initial Object (Empty Set ∅)
∅ → A (unique)There is exactly one function from the empty set to any set:
absurd : ∅ → AWhy?
Because there are no elements in ∅ — nothing needs to be mapped.
So the function exists trivially.But what about the reverse?
A → ∅ // (almost never exists)This is impossible in general. A function must map every element of A, but ∅ has no elements to receive them.
Terminal Object (Singleton 1)
A → 1 (unique)There is exactly one function from any set to the singleton.
Why?
Because all values collapse into the single element of 1.
But now, the reverse behaves very differently:
1 → A (many functions)
Each function corresponds to choosing one element from A.
Even though initial and terminal objects are dual in theory:
| Object | Incoming ar rows | Outgoing arrows |
|---|---|---|
| ∅ (Initial) | none | unique |
| 1 (Terminal) | unique | many |
Types of Functions — Where Symmetry Survives (and Where It Breaks)
To truly understand asymmetry, we need to look at how functions behave.
Not all functions are the same. Some preserve information, some lose it, and only a few are perfectly reversible.
Injective (One-to-One)
a₁ ≠ a₂ ⇒ f(a₁) ≠ f(a₂)Different inputs always produce different outputs.
Intuition:
Example:
fun double(x: Int): Int = x * 2• 1 → 2
• 2 → 4
No two inputs give the same output.
But…
Even though nothing is lost:
Surjective (Onto)
∀ b ∈ B, ∃ a ∈ A such that f(a) = bEvery value in B is covered by the function.
Intuition:
• Output space is fully covered
• But multiple inputs may map to the same output
Example:
fun parity(x: Int): Boolean = x % 2 == 0• Many inputs → same output (true or false)
You cannot tell which input produced the result.
Bijective (Perfect Symmetry)
A ≅ BA function is bijective if it is:
• Injective (no collisions)
•Surjective (covers everything)
What it means
Intuition:
• No information is lost
• No ambiguity exists
• Fully reversible
Example:
fun encode(x: Int): String = x.toString()
fun decode(s: String): Int = s.toInt()You can go forward and backward safely.
Why only bijections are truly symmetric
Only bijections allow:
A ⇄ BForward and backward behave the same
Final understanding
| Type | Information Loss | Reversible |
|---|---|---|
| Injective | No | Not always |
| Surjective | Yes | No |
| Bijective | No | Yes |
A function is symmetric only when nothing is lost and nothing is duplicated — that’s exactly what a bijection guarantees.
This is the core reason why:
Most real-world systems are asymmetric — because most functions are not bijections.
Programming View — How Asymmetry Shows Up in Real Code
So far, we’ve explored asymmetry in theory. Now let’s see how it naturally appears in everyday programming.
At the core, everything comes down to one idea:
Functions either preserve information or lose information
And this is what creates asymmetry.
Embedding (small → big)
val f: (Unit) -> Int = { 42 }• Unit has only one possible value
• Int has many possible values
So this function doesn’t “compute” anything from input.
Important clarification
Even though this function always returns 42:
• The type Int contains many possible values
• This function picks one of them
Embedding places a value into a richer space without losing structure
Collapsing (many → one)
val g: (String) -> Unit = { Unit }• String has many possible values
• Unit has only one
So:
Result
• Information is lost
• Different inputs become indistinguishable
Collapsing destroys structure by removing distinctions
Composition — where asymmetry grows
collapse ∘ collapse = more collapseEach transformation may lose some information.
When combined: The loss accumulates
val result =
user
.name // lose structure
.length > 5 // lose even moreBoolean // Final result
• You started with a rich object (User)
• You ended with a simple value (Boolean)
You cannot go back
Summary
Even though category theory gives us dual definitions, the real world (functions) breaks symmetry.
