Don’t mistake types for good Abstractions
I wanted to add headers to an HTTP request, in Scala. Soon Realised, I can’t create them — instead, I needed to parse them. Once I did that, it returned me headers in a monadic type, which I then needed to unwrap with pattern matching. Oh boy! this was complex. Not exactly the kind of abstraction I was hoping for.
I saw two problems with the implementation. First, too many intermediary steps. Second, mistaking types for abstraction.
Let me elaborate on this with another example,
a = f(x)
b = g(a)
Here I wrote down the exact sequence of steps.
First, Call f, remember the result, then call g with that result.
Also, call to g should not happen before execution of f completes.
Like headers example, this program forces people to think in small incremental steps.
With no sight of the final state, all one can do is take one little step at a time. This is an imperative code
Problem one: Imperative thinking
If you think about it, this is how the program is actually executed at the hardware level (CPU registers, RAM, BUS, caches, etc.). Program execution moves data from hard disk to CPU registers. But, CPU architecture limits this movement, which means data can be transferred at 32bit or 64bit at a time in the system bus. If you have only one core, it is a sequential process. In another word, this is an imperative style of programming.
This programming model comes from architecture that was designed to address the limitation of computing machines in the 1960s.
It has also permeated the way we write programs. It has turned into an intellectual bottleneck that has kept us tied to word-at-a-time thinking instead of encouraging us to think in terms of the larger conceptual units of the task at hand.
Though it will take a long time to free ourselves from Von Neuman architecture, modern compilers hide this complexity away from us. Yet the onus lies with us, to design so that users can easily grasp the whole picture.
The human brain can hold a limited amount of information in its working memory (7 items +/- 2). If I have to hold half a dozen types in my head, it is already a bad abstraction. A hallmark of imperative thinking style.
Problem two: Unnecessary dependencies
Another problem with this design is that it generates many unnecessary types/dependencies. One simple way to avoid this is by only dealing in primitive types, but
Most interesting objects end up doing things that can’t be characterized by primitive alone
– Eric Evans (DDD blue book)
To solve this problem more elegantly, Erik Evans encourages a pattern called “Closure of operation”.
If we take two real numbers and multiply them together, we get another real number. Because this is always true, we say that the real numbers are “closed under the operation of multiplication”: there is no way to escape the set. When you combine any two elements of the set, the result is also included in the set.
– The Math Forum, Drexel University
2 * 2 = 4
Property of closure of operation provides us with a way of defining an operation without involving any other concepts or types ( monoid ???)
Parting thought, Functional programming can guarantee the correctness and composability of the program. It doesn’t, however; guarantee understandability. For that, you need to think in abstractions and not in types.