The smartest people in our field are digging in the wrong direction.
While brilliant minds burrow ever deeper into type systems—adding dependent types, effect systems, and increasingly sophisticated type-level programming—we’re missing the forest for the trees. We don’t need smarter types. We need a better substrate for building composable software. We need LEGO parts.
Functional Programming: A Dead End for Composability
Let me be direct: functional programming, despite its elegant mathematics and appealing purity, is a dead end when it comes to achieving true LEGO-like composability in software.
The problem isn’t what FP does—it’s what it actively prevents. FP is suitable for expressing one kind of program: a calculator, where the result is the only thing of importance. In today’s world, with today’s hardware, the result isn’t the only item of interest. We care about how we get there—relative timing relationships, sequencing, parallelism, concurrency. We’re building for the Internet, robotics, IoT, distributed systems.
Yes, we can figure out workarounds to accomplish all of these things and force-fit them into a warped version of the functional paradigm. But it’s better to actually solve the real problem instead of expending brain power on workarounds.
What We Actually Need
To build software from true LEGO parts, we need five fundamental elements:
1. A Super-Simple Transport Mechanism
Not a labyrinth of complicated type parameters. Not GADTs or higher-kinded types. Just a straightforward way to move information between parts. The complexity budget should go into what we’re building, not into the plumbing.
2. Recursive Part Definition
Think of it this way:
This is like Lists and Atoms in Lisp, but instead of data types (numbers, symbols, strings, lists), these are types of code structure. Containers within containers, all the way down—until you hit a leaf that actually does something.
3. Parts That Aren’t Just Filters
The functional approach fetishizes filters: one input, one output, data flowing through like water through pipes.
But LEGO blocks don’t only connect in straight lines. You can combine them in countless ways—sideways, stacked, branched. Real software parts should offer the same freedom. Sequential pipelines are just one way to connect parts, not the only way.
4. Pure, Asynchronous Message-Passing
Here’s where functional programming reveals its fundamental flaw: FP only gives us impure, synchronous message-passing.
When a function calls another function, it blocks. It waits. It suspends. This is synchronous operation by definition—it can never be truly asynchronous.
This is synchronous control flow. The synchronous nature of data delivery in the paradigm expects that action or reaction to the data be forced to happen immediately, while the caller waits/blocks. This is the fundamental nature of the functional paradigm. We can find workarounds like adding queues, but the paradigm itself discourages better approaches by making us work harder to imagine workarounds. The idea of synchronicity is built into the notation. We need a notation that has asynchronicity built into it at a fundamental level—one that encourages us to think that way instead of discouraging thoughts in these directions. Today, most programming languages cause us to write code with a built-in synchronous assumption: we hard-wire the code to know that a result will be returned before it can proceed. In a fire-and-forget paradigm, we wouldn’t write code that way, nor would we end up in inconvenient places like callback hell.
What we actually need are software units that fire-and-forget: asynchronous message passing with no constraint on when the message will be processed. Pass data only, with zero assumptions about timing.
But here’s the thing: when timing does matter, we should be allowed to reason about it and express it. Protocols, state changes, statecharts—these are fundamental to real-world software. The FP paradigm forces us to hide this behavior in the bowels of operating systems and to find workarounds that force-fit expression of timing and sequencing into the functional paradigm. Since FP gives us only one way to think about control flow (sequential), we have to run the fans in our brains on High to find workarounds.
Ever wonder why we need operating systems? They’re elaborate workarounds for the functional programming concept.
An OS converts long-running synchronous flows of function-calling-function behavior into state machines by silently saving state somewhere deep in the system (process descriptors, stack frames, registers).
But here’s the kicker: the Software Architect gets no say in how this synchronous flow is chopped into states. The partitions are determined dynamically by an algorithm at runtime, and they change over time.
Dynamic anything is harder to debug. When you can’t control how your code is partitioned into states, you’re left scratching your head when problems appear.
Protocols that run silently underneath our software aren’t good enough. We need better control. If we want to buy-in a fancy algorithm, we should be able to plug it in like a LEGO block instead of compromising with what the operating system has deemed is needed in our specific case, or by futzing with a zillion parameters and options and priorities and unforeseen gotchas.
5. Ports and Gates
Information flow must not cross the boundaries of parts willy-nilly. Flows can only connect to ports at the edges of parts. The outside connection point is a “port”; the inside connection point is a “gate.”
This enforces true encapsulation at the architectural level, not just at the code level.
UNIX Pipelines: Good Start, Wrong Direction
UNIX pipelines gave us a taste of LEGO parts, but only along a single dimension. They’re inspired by functional thinking: one input, one output, rendezvous semantics, no fan-out.
cat file | grep pattern | sort | uniq
Beautiful, yes. But limited.
We need to build on these ideas and modernize our workflow. We need to add fan-out, even if it offends FP purists.
Why Fan-Out Matters
Fan-out makes developer experience vastly simpler:
You can lasso a bunch of parts and abstract them into another layer
That layer can have as many or fewer input and output ports as the original
Fan-out makes it possible to build true black boxes
One input on the outside
You don’t care how many places it goes on the inside
The box is “black”—you can’t see inside, you only see it as one thing
If the architect did their job right, the black box appears simpler than what’s inside.
This is exactly what happened with computers and CPUs. We had big blobs of thousands of electronic components (transistors, tubes). Then someone produced a “narrow waist”—80 opcodes instead of thousands of transistors. From that simplification, many people could build electronic devices and discover new combinations.
Why Black Box Abstraction Fails Today
Black box abstraction is essential for the LEGO part approach. So why does it fail in current development workflows?
Functional programming leaks control flow.
When a calling function blocks (control flow) while calling another function, it creates hidden coupling. That coupling causes black box abstraction to fail.
The rendezvous model—the expectation of synchronous, blocking interaction—plus the filtering model (1-in, 1-out) greatly restricts the possibilities for abstraction.
You can’t build arbitrary LEGO structures if every connection forces a blocking rendezvous.
The Cognitive Dissonance of Functional Programming
Let’s be honest about what we’re actually working with.
Synchronous, sequential programming is just a glorified form of assembler, inspired by how CPU ICs work. But CPUs are just building material. We need new ways to express what we want to build. Then, and only then, can we try to map those new expressions onto CPUs and sequencers.
Here’s the irony: functional programming actually violates the realities of CPUs. CPUs are bolted to RAM. CPUs are meant to mutate RAM—that’s their fundamental design. To think in terms of the functional paradigm, you have to prohibit mutation. Cognitive dissonance.
At its core, FP is just a fancy, complicated way to perform find-and-replace. We’ve built elaborate edifices of type theory on top of what is essentially pattern matching and substitution.
The Path Forward
We don’t need smarter type systems. We need simpler, more powerful primitives for composition:
Asynchronous, fire-and-forget message passing
Container/leaf part hierarchies
Fan-out and arbitrary connection topologies
Strict port-based boundaries
Zero hidden control flow
The ability to express and reason about timing when it matters
The mathematics of type theory is beautiful. But beauty without utility is just decoration.
It’s time to stop digging deeper into type systems and start building the substrate that will let us construct software from true LEGO parts—parts that snap together easily, parts that hide their complexity, parts that compose freely in any direction.
The future of software isn’t in proving more theorems about types. It’s in making software that fits together like LEGO bricks.
For a sketch of how black-box abstraction can simplify presentation of complicated ideas, see my article on PBP Black Box Abstraction, Ports, Fan-out.
What are your thoughts? Are we over-investing in type sophistication at the expense of compositional simplicity? Let me know in the comments.
See Also
Email: [email protected]
Substack: paultarvydas.substack.com
Videos: https://www.youtube.com/@programmingsimplicity2980
Discord: https://discord.gg/65YZUh6Jpq
Leanpub: [WIP] https://leanpub.com/u/paul-tarvydas
Twitter: @paul_tarvydas
Bluesky: @paultarvydas.bsky.social
Mastodon: @paultarvydas
(earlier) Blog: guitarvydas.github.io
References: https://guitarvydas.github.io/2024/01/06/References.html