Untangling programming paradigms

Untangling programming paradigms

I think the biggest pitfall with programming paradigms is not picking the "wrong" one – they all have their place – but using a paradigm in a wrong way. It's a problem I've had myself and that I've seen others have too. Now, there are a bunch of programming paradigms out there and I will not argue about which one is best, I just want to talk about this pitfall I noticed.

To make sure we're on the same page, I need to scope things. I'm going to focus on the imperative, object-oriented (OOP) and functional paradigms since I have at least some experience with all of them. For the sake of the discussion, the paradigms need to be ordered the following way: imperative -> OOP -> functional.

Notice that, as we progress through this order, the paradigms get more restrictive and rigorous. It also coincides with the order in which they got hyped by the programming community.

A common pitfall is to say that you are doing OOP but actually have your code organised in an imperative way. Sure, the objects are there, but there is barely any encapsulation of functionality, most of the methods are public, called in-sequence, potentially in multiple parts of the codebase in ways that are just slightly different from one another. A recipe for unmaintainable code.

class SompeProcessor {
  private final SomeHelper helper;

  public void doStuff(X x, Y y) {
    var z = helper.buildZ(x, y);
    helper.doThis(x, z);
    helper.doThat(y, z);
    helper.wrapThingsUp(x, y, z);
  }
}

The point of OOP

The way I see it, the OOP paradigm tried to address the following problems of the imperative paradigm:

  • State is global, anything could change anything else.
  • Anything can be used anywhere else. You just need to know the function name and then you can call it.

Objects are supposed to fix these issues. You can encapsulate state in an object and then only let that object change its state. You still have state, but it's now hidden from most of your program. It can only be changed through the interface of that object.

OOP also helps you think in more abstract terms. Imperative programming is more concerned with the how (first do x, then y and finally z) and OOP helps you focus on the what (we have A, B and C and they interact with each other). It helps you chip away at the complexity of your system little by little, both in terms of problem-solving but also in terms of code organisation. Through the means of encapsulation and abstraction you reduce coupling between different parts of your code.

thinking you are doing OOP but actually being stuck in imperative thinking can lead to tight coupling. The SomeHelper class is there but SomeProcessor is aware of a big chunk of its internals

On the other hand, if you lean into the OOP paradigm, then you can achieve lower coupling between pieces of your code and have a clearer understand of what each "thing" is responsible for.

class SomeProcessor {
  private final SomeHelper helper;

  public void doStuff(X x, Y y) {
    helper.doComplexHelp(x, y);
  }
}

SomeProcessor has fewer dependencies (on both classes and methods) and less code now. The distribution of responsibilities is also much clearer, SomeHelper is clearly the place that is responsible for the details of the behaviour, not SomeProcessor. Changes of the various implementation details will not affect SomeProcessor so long as the doComplexHelp(X x, Y y) method signature remains untouched.

SomeProcessor has fewer dependencies now (both on classes and methods). Parts of SomeHelper can be refactored without affecting SomeProcessor.

The point of functional programming

Full disclosure, I have little practical experience with functional languages but the way I see it, they focus on 2 topics. Having full control over where side effects happen and providing very expressive type systems. We're going to focus on the side effects part.

To me, the popularity of functional approaches looks like a reaction to the drawbacks of OOP.

OOP gives all these fancy rules for organising your code, all these fancily named tools with all the fancily named toolboxes to organise your tools in and at the end of the day, you still can end up with a confusing ball of mud on your hands. You tweak one class, and then the tests for a bunch of other classes are broken... nice.

So, concepts alone are not enough, we need actual rule enforcement here! And we're not talking about frameworks or libraries that help you assert this stuff, no, they can be sidestepped. We need the language itself to be more vigilant!

Long story short, in languages like Haskell you have a clear separation between pure functions, and functions that can produce side effects or that rely on side effects. The construct used to allow side effects to happen is called a monad. Functions that do not have a monad in their signature are side effect-free. In your functional-paradigm program you want to have as much of your logic as possible inside of side-effect-free functions, they're called pure functions.

What's the trap that OOP programmers can fall into when they try to use a functional language? Huge monads. Monads are scary, so once you managed to set one up it's easy to just put as much code as possible into it because otherwise you need to figure out again how to make the jump from your pure functional code back into the monad bubble where side effects are allowed. Doing this correctly requires you to get comfortable with the various concepts involved, and to understand how they operate. Because unlike OOP, where concepts are just empty shells that you can fill with your own ideas, in functional programming most concepts have behaviour. A monad is not just a "thing", something is a monad if its type looks a certain way and if certain operations have been defined for it. The behaviour is part of the language level concept. On the other hand, OOP gives you interfaces and classes. That's basically it, it's up to the programmers to create further concepts. Great freedom! But also great responsibility.

Combining different paradigms

Combining different paradigms can be beneficial, you just have to be careful to not let old thinking patterns confuse you. In particular, I think that introducing "functional islands" inside of OOP code is great, it makes it easier to reason about the code and to write tests. You just have to be careful and make sure that you are actually writing functional code there. It is easy to think you are writing a pure function but actually be modifying stuff that you have access to. If you have a java.util.ArrayList , nothing is stopping you from modifying it inside of a filter() or a map() stream operation!