Programming Without State Chaos. At OutSystems, we strive to keep our…

By Afonso Matos

Afonso Matos

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 breaks local reasoning.

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

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

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

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

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.

Before:

After:

Side Effects and IO Operations

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.

Before:

After:

Sacrificing Purity

Unit Testing Pure Functions

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.

Wrapping Up

  • 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.