SOLID Principles and Functional Programming
In this post I would like to explore if SOLID principles (a.k.a Object Oriented Design Principles) are useful in a functional programming context. SOLID principles are not sacred commandments that need to be followed dutifully. They are guidelines for designing object oriented code.
All software development activity is design activity. When I say design in this post though, I mean the more abstract design activity that happens (in developer’s head, on paper, etc.) before coding begins.
Java got lambdas now, Kotlin is growing stronger with lots of functional features… Is there anything functional programmers can learn from OOP design?
Means of Abstraction
Fundamental mechanism for abstraction in functional programming is function. All general purpose functional languages also provide some form of modules for building abstractions. Haskell has ADTs, type classes and modules. Scala has classes, objects, traits and packages. Clojure has multi-methods, protocols and namespaces.
In object oriented languages there is typically one mechanism for abstraction; class. A running program is a (dynamic) graph of instances. These instances are created from the classes catalogued in the program code.
With this understanding, let us start our exploration of SOLID:
You should be able to extend a classes behavior, without modifying it.
This principle can be observed in many of functional operations.
(defn filter-odd [coll] (filter odd? coll))
Above code defines a function filter-odd that takes a collection (of numbers most likely) and returns another collection with only the odd numbers in it. As you can see in the function body filter takes a predicate to define the filtering criteria. As long as we have odd? defined, we do not need to define a new function (filter-odd). In fact it would make the code less readable to use (filter-odd coll) form instead of (filter odd? coll) form. With the former form, we would not know what else filter-odd might be doing without reading its source (or its documentation). With the latter form, even if we do not know exactly what odd? is, we know it is supposed to be a predicate function.
Open-closed principle can be extended to higher levels of abstraction too. When we design libraries we try to support as many use cases around the core functionality as possible. Paying same kind of attention to extensibility of the modules within our library (or application) would enable us to change our codebase without introducing too much instability. In other words: to change our program’s behaviour we would be adding new code more and changing existing code less.
Many Lifes of Code
Code is alive when we run it. But it has another life; at rest, when we are developing it. And it has another life: when it is observed as a black box without running it as it is meant to be run. I will explain.
The first life (or lifes) is all about functionality. When we observe it, it is either to make sure it is doing the right things or whether it is in a healthy environment so it will keep doing the right things. Or maybe we do not observe it at all. Regardless this is when code is fulfilling its primary purpose.
The second life is all the shenanigans developers perform to make the first life a reality. An amateur mistake is to focus on just making it work. While this theoretically works (pun intended), there are shortcomings of this approach on multiple levels. This deserves a whole blog series but I will mention just one issue with this approach: if you fiddle with the dials until you get the desired effect, without knowing why you are doing what you are doing, you will never grow as a programmer. Eventually you will hit a complexity ceiling (individually or as a team) and no amount of fiddling will produce the expected results.
So, intentionality is important. What would logically follow from that is code is not just instructions to the machine. The structure of code has a large influence on how we build our programs. If we do not get it right, we may end up in an architectural dead end. Backtracking in this case may cost a lot. Also there are cognitive costs of understanding the structure, before we can do any meaningful change. With a bad design, cost of figuring out what to do may be much larger than the cost of making the change. Let us keep these in mind, but continue with the third kind of life.
The third life of a code is when it is seen as a product with some potential to produce value. In the first kind of life there is no observer, or maybe the observer is the system administrator. In the second kind of life the observer is the developer. In the third kind the observers are people who use/test/evaluate a library or application. They will need a high level understand of what the library or application does. For this they will refer to the documentation and examples provided. And ideally they will never need to refer to the source code. It is not because reading source code is bad. It just shows that the documentation and the examples are not enough to learn enough about the library or program.
The Interface Segregation Principle:
Make fine grained interfaces that are client specific.
The Interface Segregation Principle (ISP) is about informing the stakeholder only as much as necessary to do what they need to do. Within the code this allows the developers to decouple components (read this as reduce complexity) and also help decrease their cognitive load when they need to use another component. In the form of documentation and examples this means sparing the observers from reading about things that do not concern them. In this sense ISP is applied by hiding internals. In OOP, interface usually means a very specific thing. In functional programming context we should extend the application of ISP to any kind of contract. Amateur functional programmers with their focus on just making it work, often neglect hiding internals of their modules. This does not look like a problem to them. But it hurts them later as increased cognitive load. And it hurts people who are reading the documentation or trying to write tests as well.
Functional programmers can learn something from OOP software design literature. With the necessary contextual modifications SOLID can be applied to functional programming. But my conclusion is that as functional programmers we can and should pay a little more attention to component level software design. Most literature out there focuses on the language and the lowest level of design, completely neglecting overall application design. And I believe application design is mostly agnostic programming language paradigms. If you are one of those just make it work developers I hope this post inspires you to investigate into application design a little more.
If you have any questions, suggestions or corrections feel free to drop me a line.