At OutSystems, we strive to keep our codebase modular and easy to understand, so we can quickly improve parts of our systems and components without requiring too many changes. One of the challenges we face is programming without state chaos. As you shall see, state is a good candidate for the deepest cause of software complexity and lack of modularity. Why is it a problem? How can we manage it? Here are some answers.
State Is Hard
State is anything that can change. When code depends on state, you multiply the complexity a thousand fold and you can’t easily reason about the code; you have to check the internals and look under the hood. Thus, modularity is diminished.
Check out this simple piece of code:
When hitting a bug, this line won’t even be on your suspect list. But after debugging you find that for some odd reason, this piece sometimes is not working as expected. Your local reasoning and intuition told you that the function
isElementClickable must be self-contained, but after looking at its internals you find that’s not the case.
You ask yourself “How could I know that the function relied on a changing state?” Well, you couldn’t! Only by looking at its internals.
Does this sound familiar to you? This issue is so common that it’s generally seen as just plain programming. The complexity grows exponentially when the dependencies on the external state start growing.
Now, to understand a piece you have to understand the whole. You aimlessly navigate through the code to find what is happening. After asking your peers “Does anyone have context on this?”, you finally find out a sequence of state mutations that causes your bug, and then dismiss the complexity as a lack of context.
There must be a better way.
Pushing State Outwards
Push your state outwards like your life depends on it. This means that the core of your program should always be as pure and immutable as possible, in the sense that it does not depend on state — in contrast to impure and mutable, which relies on it.
By following this principle, you ensure that the crux of your program is completely predictable. Your functions are 100 percent self-contained. There is no hidden circus inside. They are akin to mathematical functions: getting an input and returning an output. This style of programming with pure functions is called functional programming.
In the previous example, an obvious approach to enlarge our immutable core would be to make
isElementClickable pure by pushing the state outwards.
Now you can relax and trust that
isElementClickable will only depend on what you give it. If it’s returning something unexpected, the problem is only within that function and nowhere else. There is no external influence. You don’t need to know about the whole world. Modularity wins!
The risk of showing small examples is that one can easily dismiss them as obvious. But the real-world scenarios are just inflated versions of this same problem.
Functional Programming In a Stateful World
We the developers, on our mission to translate the world to software, tend to bring state along everywhere we go. Let’s look at a bank. We immediately think of Account, Transaction, and Person with an internal changing state. For us, a data-structure account that momentarily changes its balance from $1000 to $500 makes sense, so much that we created entire programming languages around this concept and called it Object-Oriented Programming (OOP).
In traditional OOP languages like Java or C#, it’s natural to use a mutating state. The whole point of OOP is to have little boxes with their own state.
However, in the functional world, we prefer to keep the confusion out. There is no state. To somehow change the balance of an Account, we would need to create another Account. In the functional world, it's perfectly reasonable to do that.
Immutable Data Structures
Take the example of DateTime from .NET. It's an immutable data structure, hence with no state. All the methods can be considered pure functions. Every time you want to change the date, you actually create a new one.
You might be wondering, “So do I keep cloning everything?” That’s because your data structures are mutable. If there is no possibility of mutation (which means no state), you can simply reuse them (or part of them) and still maintain your purity. In the next part of this series, we’ll dive deeper into immutable data structures.
Avoid State Inside Functions
The bigger issue is when functions depend on external state, but we can also avoid it inside functions. If they have complex logic, keeping track of local state in your head can make it harder to reason about the code. By turning all of our code pure, we bring modularity to programming itself.
In almost all modern languages, a lot of constructs and auxiliary functions (like map and reduce) have been added to help this effort.
The following example shows a simple way of clearing your code of local state.
Side Effects and IO Operations
Pure functions have no side effects (whenever you call a function, it has no other effect rather than returning a value). Having a pure function means that nothing outside will change, nor will it depend on things changing outside.
However, side effects are the interactive part of our programs. Examples of side-effects include: fetching users from a database, printing something to the screen, and getting any sort of input from the users of our program.
If there are no side effects, our programs are black boxes that don't interact with the outside world. Mathematical beauties? Indeed, but empty of practical value.
The right approach to incorporate functional programming into your codebase is to make the core of your programs functional, and allow some leeway on the outermost layer. The thinner the outer layer, the thicker the functional core, which will guarantee program correctness, easy reasoning about the code, and modularity.
All IO operations cause side-effects by nature, so following our principle we also try to push them to the peripheries of our program as much as possible.
Sometimes you might need to sacrifice purity for performance reasons, limitations of the language, or sheer simplicity. In such scenarios, you can strive to encapsulate this state in such a way that its result is indeed a pure function. In the getEvens example, the function was already completely pure, even though internally it had state.
Unit Testing Pure Functions
If pure functions are analogous to mathematical functions, then testing them should be as easy as checking the return value for the arguments passed. No need to use spies, remember to set up a global state, or check for hidden treasuries. Pure functions only do one thing: return a value. So that’s the only thing you should look out for.
Next time your colleagues ask you for a code review, if it’s the case, just say that their code looks damn pure. If they don’t get it and give you a funny look, link them to this article.
Now you’re ready for the real battle: to get hold of all the state across your codebase and bring it out of the shadows. Let’s review the main points:
- State increases complexity and breaks local reasoning
- The solution is to push state outwards and the corollary is to enlarge the quantity of pure code
- If you must use local state, encapsulate it in pure functions
- Side-effects/IO are needed, but can be kept on the peripheries of our program
- Testing pure functions is a matter of checking the return value
And that’s it!
In this blog post, we have barely scratched the surface of a topic that is a pillar of functional programming. There are many more benefits that immutability can bring to software. If you are interested in working in software that strives to imbibe these principles, consider joining us at OutSystems.