SOLID principles in Functional Programming

Maciej Kocik
6 min readApr 10, 2022

--

Functional programming is getting more and more popular nowadays, bringing some fresh concepts to the table. Thinking in a ‘functional’ way may be difficult for a person who is used to working in object-oriented environments. It requires an adaptation process — not only to learn a language but also to do a kind of mind-switch, not to see the programming world in terms of objects and variables.

My story relates to the above sentence, as in most of my professional career I’ve been involved in projects written with the usage of OOP. I’ve started discovering a world of purely functional programming a few months ago, and I believe I went through the transition process smoothly. Although it’s not always easy to envision the solutions in a new, more functional light.

Clean code

There are a lot of debates on a clean code. Which patterns should we use, and how should we shape our solutions to be decent ones? I noticed that most of those discussions revolve around OOP. Most of the examples and patterns use OOP as a base, and they also target the audience that way. I am an enthusiast of using SOLID principles, so I’ve started wondering how we can apply them to the world of functional programming. The following article summarizes both things: my thoughts and the research I did regarding this topic.

What is SOLID

As you read this far, the SOLID principle is probably not a new thing for you. But to be exact, let me introduce the meaning of this term.

SOLID is an acronym created by Robert C. Martin (Uncle Bob), standing for:

It establishes the practices for developing highly reliable and maintainable software. Often it is called the principles of Object-Oriented Programming. Is it true? Let’s check what Robert C. Martin says about it himself:

So it applies to the non-object-oriented world as well. How does it?

Single Responsibility Principle

In the OO world, this principle is often being interpreted in a way that the class/object should have only one responsibility, and should do only one job. It’s partially true. The actual definition says:

“The class should have one and only one reason to change”

or put in other words

Gather together the things that change for the same reasons. Separate things that change for different reasons.”

If there is a requirement for a change coming from the business unit, modification in the class shouldn’t affect any other areas, as the class has a single reason to be changed. If the accounting service calculates the salaries and prints them, it means it has two potential stakeholders who can request changes in the service. It doesn’t follow the principle.

How is it different for FP? Not much. Although we don’t operate on classes, functions should be separate units of code, implementing one use-case with a singular reason to be changed. This principle is even more accurate in the functional world than in an object-oriented one. We use pure functions naturally. The function composition is also widely used. It gets intuitive to write functions with one responsibility and compose them together.

Open/Closed Principle

Software entities should be open for extension but closed for modification.

This principle states that adding new functionality to the codebase should require writing new code extending the current behaviors, instead of modifying existing modules. It advises separating abstract concepts from detailed ones. In OOP, we achieve it by using inheritance and/or composition. Working with interfaces and abstract classes makes the code more flexible and extendable.

As morbid as it sounds, this principle has its serious limitations. It’s not possible to be 100% compliant with it. It requires code extraction in favor of using abstractions, but finally, there still is a place when the concrete implementation must reside. Making everything abstract paranoiacally, just to comply with this principle, makes the code illegible. That’s why it’s important to think carefully about the system design. Use the principle in the parts that are going to change in the future.

In terms of FP, this principle applies mostly to higher-order functions. It’s a natural way of abstracting some behaviors outside of the scope of the current function. Usage of composition also falls into the definition of the Open/Close principle. Composing a few smaller functions into a new one can create new functionality, without modifying the current behaviors at all.

Liskov Substitution Principle

In 1988, Barbara Liskov wrote the following statement as a way of defining subtypes:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T

This definition became known as Liskov Substitution Principle. It sounds complicated at first, but it gets pretty simple when thinking about it for a little while. Paraphrasing it:

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

In terms of OOP, it requires us to inherit carefully from the base classes, not to break their behavior. For FP, it can be applied to generic types. Function with arguments of a generic type should ensure that the implementation works correctly, regardless of the concrete types of passed arguments.

Interface Segregation Principle

Keep interfaces small so that users don’t end up depending on things they don’t need.

It’s probably the simplest principle of all five. Creating any interfaces keeps them as small as possible. Divide big interfaces into smaller ones, to make sure every interface delivers exactly what it promises and nothing more.

In terms of FP, this principle is quite intuitive. Keep all the internal logic private and provide only the customer-facing functions outside the module. Show nothing more than what’s necessary to use your module correctly.

Dependency Inversion Principle

Entities must depend on abstractions, not on concretions

This principle is key to designing the architecture of the module properly. In a system that does not comply with this principle, the high, abstract modules (containing the business logic) rely on the low, concrete implementation details (for instance rng generators, filesystems, concrete databases, etc). The control flow goes from the top to the very bottom, where the concrete modules reside. We can use interfaces as a tool for inverting the flow of a program.

In a system designed with the Dependency Inversion Principle in mind, all the low modules are just replaceable details. The high, more abstract modules are based on the interfaces and don’t depend on any details laying below. Dependency injection is being used to inject specifics in place of the underlying interface. The flow of dependencies is inverted.

It turns out that for FP this principle is natural by design. Abstraction is nothing new under the sun of the functional programming world and the dependency inversion principle is based on abstraction. Function composition and Higher-order functions come in handy here, allowing to swap the implementation of concrete details when needed.

Summary

Thinking about every single principle, we can conclude that the functional world absorbed the SOLID principles well, and made them very intuitive in its environment. So intuitive, that sometimes they seem to be more natural in FP than in OOP, for which they were originally presented.

--

--