Table of Content
- Part 1 (this part)
- Functional Ideas
- Category theory
- LINQ, Lists and Optionals
- Part 2 (coming)
- Duality and RX Extensions
- Monads and async await
- Part 3 (coming)
- Abstract Data Types
- Part 4 (coming)
- Making Invalid States Unrepresentable
ou’ve Been Using Functional Ideas All Along—Now Understand Them
Many of the most important and widely adopted programming features of the last two decades have their roots in functional programming—and deeper still, in category theory. These ideas have spread across languages often underpinning popular features without developers realizing their origins. For object-oriented programmers, understanding these ideas can radically reshape how we think about types, structure, and behavior in software.
The Value of Abstraction
Category theory gives us a mathematical language for thinking about types, how they relate, and how they can be transformed. Once you see this, you can't unsee it. Patterns across libraries and languages start to click. Abstractions become more coherent. Features you once struggled with begin to make sense—not because you've gotten smarter, but because you're seeing the bigger picture.
Often I see programmers struggle with new features, when there are older features that have similar properties and behaviour that they have no problem with. Understanding is only a slight shift in perspective away.
My First Steps: LINQ and Composability
My first brush with category theory came with C# 3.0 and the introduction of LINQ. At the time, I was refactoring a codebase and trying to make libraries more reusable. Lists and "optionals" were giving me grief. Iteration, filtering, paging, and threading all felt like they required too much boilerplate. My object-oriented toolset—design patterns, interfaces, inheritance hierarchies—wasn’t helping.
Then came LINQ, and suddenly, things started to fall into place.
Lists, Optionals, and What They Really Are
In category theory, structures like List<T>
or IEnumerable<T>
are more than just objects—they’re functors and (often) monads. These terms may sound esoteric, but they describe powerful and consistent ways to work with data.
Imperative programmers often obsess about whether to use T[], List<T>, Dictionary<TKey, TValue> or HashSet<T> and while this is be important, it is better to consider the shape of the information at a higher level of abstraction before descending into the weeds.
In C# optionals were a mess. Calling nullable value types and reference types "optionals" is being generous. They held the same information, but the functionality fell far short of what you would expect from a Maybe or Optional type.
Nullable value types (int?
, DateTime?
, etc.) had ugly ergonomics: HasValue
and Value
were clunky, and misuse was rampant. Reference types were even worse—they were always nullable, with no type-level way to opt out to give an honest object.
You could use the new extension methods on null reference types and it helped with some things, by allowing you to sneak in null checks into some of your library components and letting you filter out null object or objects with null prosperities out. but this was trying to empty the ocean with a teaspoon. My main goto to get around null references was to use the Null Object software design pattern. However trying to do this for every reference type that might possibly be null, was a non-starter.
This wasn’t just a C# problem—it was inherited from C, which inherited it from even older languages going back to ALGOL W in 1965. Tony Hoare, the inventor of the null reference, famously called it his “billion dollar mistake.”
The crux of the problem: you’re asking humans to manually track where null
might sneak in. Humans are bad at that. The nullable reference type was a massive hole in the static type system. These systems are supposed to catch errors at compile time, but here was a whole class of errors—null reference exceptions—escaping to runtime.
LINQ: Category Theory in Disguise
If you define two extension methods—Select
and SelectMany
—with the right signatures, you can make almost anything LINQ-able. That’s the power of monads.
-
Select
(a.k.a.map
in FP circles) is for transforming values inside a context. Think: turning aList<int>
into aList<string>
, or aNullable<string>
into aNullable<bool>
. -
SelectMany
(a.k.a.flatMap
,bind
, or>>=
) flattens nested contexts: aList<List<T>>
becomes aList<T>
, aNullable<Nullable<T>>
becomes aNullable<T>
. This same idea powersasync/await
and promise chaining in JavaScript. Something similar is happening when a do try catch block intercepts an error from several layers down in the stack (This flattening behaviour is more obvious with languages with checked exceptions like Java or Swift)
?.
)—a syntactic sugar for optional objects that provides a limited form of mapping for honest properties and binding for optional properties. On the upside it works for both Nullables and reference types.?.
) was so popular it made it into many other languages under an assortment of names.Functional Programming's Influence Grows
Around the same time, other functional staples were entering the mainstream. Hadoop brought map
, reduce
, and filter
into the spotlight. C# had higher-order functions from the start, but lambdas in C# 3.0 finally made them usable without drowning in boilerplate.
LINQ’s use of extension methods also meant you could tack on new functionality to types you didn’t own—a clever trick that helped functional patterns sneak into OOP-heavy ecosystems. Without a coat of paint functional concepts are often resisted as being too procedural and not O-O enough.
Even the type system itself was changing. Parameterized types (aka generics) have roots in abstract algebra and were first seen in functional languages like ML (1973), then Ada (1977), then C++ templates (1991). Category theory takes this further with higher-kinded types—types that take types that take types.
Languages like Haskell and Scala can express these abstractions natively. C# can’t, but you can still use the ideas—just in a less elegant way.
The Big Realization
The real epiphany for me came when I saw that monadic operations acted on containers—like List<T>
and Nullable<T>
—as whole things. They respected encapsulation. I didn’t need to peek inside and mess with internals. I could compose operations, transform values, and chain computations without writing glue code every time.
Contrast that with traditional imperative code, which often tears through internal structures and violates encapsulation to “get things done.” Functional concepts gave me a cleaner, more composable way to think.
Conclusion
You don’t need to become a Haskell expert or abandon object-oriented programming to benefit from category theory. It might feel foreign at first. But it will:
- Change how you think about types and data
- Help you recognize deeper patterns across languages and libraries
- Reduce boilerplate and improve composability
- Let you write more robust and expressive code
Gaining familiarity with these concepts can dramatically improve your ability to design clean, reusable, and maintainable code—especially as languages like C# continue to adopt features born in functional programming.
If you're serious about mastering modern software development, understanding the abstractions behind LINQ, monads, and functors is no longer optional—it's essential.
Related Posts
- Complexity and Intent vs Implementation
- Persisting Data, Code and WorkFlow Tracking
- UX & DX
- Making Work Actually Flow
- Managing Images and Sounds.html
My Learning Tools
My Anki flash cards
My Self Directed Learning Tracking Boards
More Information
Videos
2007 Era
- Don't fear the Monads by Brian Beckman
- LINQ, C# Futures and Intellisense by Luke Hoban
Modern Era
- Category Theory for Programmers by Milewsk
- Category Theory by Awodey
- The Catsters
- Category Theory for Beginners by Richard Southwell