Having beer with OOP and FP

Another article on Object Oriented vs Functional Programming

Functional ProgrammingPhoto by Helena Lopes on Unsplash

A few days ago, having a beer with some of my fellow developers the discussion about Object Oriented vs Functional programming raised once again. And I still notice, specially among less experienced developers some misunderstandings and a lot of confusion going on around this topic.

This article tries, to the best of my knowledge, to bring a clear and simple overview about these two programming paradigms with a practical example.

Functional Programming

There are numerous definitions on what functional programming is, to keep it as simple as possible lets define it like:

Functional programming is the process of building software by composing mathematical functions.

A few principles lay the foundations upon which this way of thinking thrives among so many developers and is being widely adopted in recent years.

  • declarative language
  • functions must be pure
  • no shared state
  • no side-effects
  • immutable data

Let’s draw our story so that we can tackle it with both approaches and in the end compare them.

The final season of Game of Thrones is here, no spoilers don’t worry, this is the final war, battles will be fought and in the end the best warriors should be acknowledged by their bravery.



We will create a battle generator and in each time a battle is fought we will get the top 10 deadliest warriors.

Let’s start with a functional approach, go through the code below and after that we will start to break it down.

/**
* Functional programming
* During the battle each time a warrior kills an enemy his name is added to the battle result (string)
*/

/* ------------------------  Utils --------------------------------- */

const getRandomArbitrary = ({max = 300, min = 0}) =>
    Math.floor(Math.random() * (max - min) + min);

/**
* Impure Functions
*
* This function is impure because it contains a random generator call
* This means that if you call it twice with the same input you might get 2 different outputs
*/
const getBattleResult = army => new Array(getRandomArbitrary({min: 100})).fill('')
        .map(() => getWarrior(army, getRandomArbitrary({max: army.length})))
        .join(',');

/**
* Pure Functions
*
* Functions that given the same input will always return the same output and produce no side effects.
*/
const getArmy = () => ['Aegon Targaryen', 'Aeron Greyjoy', 'Akho', 'Alton Lannister', 'Alys Karstark', 'Arya Stark', 'Baby Sam', 'Balon Greyjoy', 'Brandon Stark', 'Bran Stark', 'Brea', 'Brienne of Tarth', 'Brynden Tully', 'Catelyn Stark', 'Cersei Lannister', 'Daario Naharis', 'Daenerys Targaryen', 'Davos Seaworth', 'Dickon Tarly', 'Doran Martell', 'Doreah', 'Eddard Stark', 'Elia Martell', 'Euron Greyjoy', 'Faceless Man', 'Gregor Clegane', 'Grey Worm', 'Hodor', 'Jaime Lannister', 'Joffrey Baratheon', 'Jon Snow', 'Khal Drogo', 'Loras Tyrell', 'Lord Varys', 'Lyanna Stark', 'Missandei', 'Myrcella Baratheon', 'Renly Baratheon', 'Rickard Stark', 'Rickon Stark', 'Robb Stark', 'Robert Baratheon', 'Samwell Tarly', 'Sandor Clegane', 'Sansa Stark', 'Selyse Baratheon', 'Stannis Baratheon', 'The Night King', 'Theon Greyjoy', 'Trystane Martell', 'Tyrion Lannister', 'Tywin Lannister', 'Walder Frey', 'White Walker'];

const getWarrior = (army, position) => army[position];

const getKills = battleResult => battleResult.split(',');

const getKillsByWarrior = kills => kills
        .map(warrior => warrior.trim().toLowerCase())
        .reduce((kills, warrior) => {
            kills[warrior] = (kills[warrior] || 0) + 1;
            return kills;
        }, {});

/**
* Declarative example
* Describe WHAT is happening, doesnt mutate state, readable at a glance.
*/
const getTop10 = kills => Object.keys(kills)
    .map(warrior => ({warrior, kills: kills[warrior]}))
    .sort((one, other) => other.kills - one.kills)
    .slice(0, 10);

/**
* Almost Pure Functions
*
* This functions logs to the console this is a side effect and that makes the function impure
* I might argue that log operations is the only responsibility of the function
* and being called with the same input will always log the same output
*/
const printTop10 = top10 => {
    console.log('The top 10 warriors in FP');
    console.log('-------------------');

    top10.forEach(({warrior, kills}, rank) =>
        console.log(`${rank + 1}. ${warrior}: ${kills}`));
};

/* ------------------------  Program (Top Level) ------------------------------- */

const startWar = () => {
    const battleResult = getBattleResult(getArmy());
    const killsByWarrior = getKillsByWarrior(getKills(battleResult));
    const top10Warriors = getTop10(killsByWarrior);

    printTop10(top10Warriors);
};

startWar();

Declarative

“Imperative programming is like how you do something, and declarative programming is more like what you do.”



“Many (if not all) declarative approaches have some sort of underlying imperative abstraction.”

The declarative form is well expressed in the code above, as you loop through the functions declarations and calls you notice that they tell you more on what to do instead of how to do it.
Notice that there is kind of an imperative abstraction layer that hides some imperative implementation.

const getTop10 = kills => Object.keys(kills)
    .map(warrior => ({warrior, kills: kills[warrior]}))
    .sort((one, other) => other.kills - one.kills)
    .slice(0, 10);

Take by example the function getTop10, they tell you that we go through all warriors, count their kills, sort them and take the first ten at a very high level without being very specific on how to do this. Later on we will check the same block in an imperative format.

Pure functions

A pure function is a function that given the same input will always return the same output and produces no side effects.

The functions getWarrior, getKills, getKillsByWarrior and getTop10 are all pure as they fit perfectly the above criteria.
getBattleResult is an impure function because it contains a random number generation inside, this means that if called twice with the same input it will (probably) return a different output.
printTop10 is another impure function, even though it satisfies the first criteria of pure functions it fails to do the same on the second one, logging to the console is a side effect since its observable outside the scope of the function.

(No) Shared state

Shared state is state which is shared between more than one function or more than one data-structure.

There is no shared state in our application since there is no global state and all functions receive the data they need by param and not accessing any outside data.

(No) Side Effects

Side-effect is any external effect a function has besides its return value.

Logging is a side effect in our application as we seen before, other than that our application has no side effects.

Immutability

Immutable object is one whose state cannot be modified after it is created

You might have already noticed but our code does not have a single let or var variable and its all composed by const variables (const variables sounds a bit confusing 😕).
Of course internally inside some functions (ex: reduce)there are values being changed but this happens at a lower level that what we are treating.
From our Program (Top Level) no variable is reassigned, mutated or altered in any manner and this is the line that matters here.

Object Oriented Programming

Take a look now at the OOP approach and in the end let’s compare the two and take some conclusions.

“… a programming paradigm based on the concept of “objects”, which can contain data, in the form of fields(often known as attributes), and code, in the form of procedures (often known as methods).” (in wikipedia)

The above citation gives us a good overview on the nature of OOP. Another relevant side from this approach is that it allows developers to reason about a program like they are reasoning about real life, being objects (classes) like real-life entities that have knowledge and can do work and live an independent existence.

Like the functional approach, object oriented also sets a few main principles, in this case there are 4:

  • Encapsulation
  • Data Abstraction
  • Polymorphism
  • Inheritance

For our practical implementation we will tackle again the same scenario used for the functional approach and after it will do the same analysis.


/**
* Object Oriented
* During the battle each time a warrior kills an enemy his name is added to the battle result (string)
*/

/* ---------------------------  Utils  ------------------------------------ */

const getRandomArbitrary = ({max = 300, min = 0}) =>
    Math.floor(Math.random() * (max - min) + min);

/* ------------------------  Initial Data --------------------------------- */

const warriorsNames = ['Aegon Targaryen', 'Aeron Greyjoy', 'Akho', 'Alton Lannister', 'Alys Karstark', 'Arya Stark', 'Baby Sam', 'Balon Greyjoy', 'Brandon Stark', 'Bran Stark', 'Brea', 'Brienne of Tarth', 'Brynden Tully', 'Catelyn Stark', 'Cersei Lannister', 'Daario Naharis', 'Daenerys Targaryen', 'Davos Seaworth', 'Dickon Tarly', 'Doran Martell', 'Doreah', 'Eddard Stark', 'Elia Martell', 'Euron Greyjoy', 'Faceless Man', 'Gregor Clegane', 'Grey Worm', 'Hodor', 'Jaime Lannister', 'Joffrey Baratheon', 'Jon Snow', 'Khal Drogo', 'Loras Tyrell', 'Lord Varys', 'Lyanna Stark', 'Missandei', 'Myrcella Baratheon', 'Renly Baratheon', 'Rickard Stark', 'Rickon Stark', 'Robb Stark', 'Robert Baratheon', 'Samwell Tarly', 'Sandor Clegane', 'Sansa Stark', 'Selyse Baratheon', 'Stannis Baratheon', 'The Night King', 'Theon Greyjoy', 'Trystane Martell', 'Tyrion Lannister', 'Tywin Lannister', 'Walder Frey', 'White Walker']

/* ---------------------------  Classes ----------------------------------- */

/**
 * Character - A superclass
 */
class Character {
    constructor(name) {
        this.name = name.trim();
    }

    getName() {
        return this.name;
    }

    showInfo() {
        console.log(`I am ${this.name}, a character of GoT`);
    }
}

/**
 * Warrior - A warrior is a special case of a Character (inheritance)
 */
class Warrior extends Character{
    constructor(name) {
        super(name);
        this.kills = 0;
    }

    getKills() {
        return this.kills;
    }

    addKill() {
        this.kills = this.kills + 1;
    }

    showInfo() {
        console.log(`I am ${this.name}, a Warrior in GoT and I have killed ${this.kills} enemies!`);
    }
}

/**
 * Army - Responsible for the army management and keeping the data related to the army
 * The army has an array of instances of Warriors has one of its properties (shared state)
 */
class Army {
    constructor(warriorsNames) {
        this.size = warriorsNames.length;
        this.warriors = Army.assembleWarriors(warriorsNames);
    }

    static assembleWarriors(warriorsNames) {
        let warriors = [];

        for (let i = 0; i < warriorsNames.length; i++) {
            const name = warriorsNames[i];
            const warrior = new Warrior(name);
            warriors.push(warrior);
        }

        return warriors;
    }

    getSize() {
        return this.size;
    }

    getWarrior(name) {
        return this.warriors[name];
    }

    getTop10Warriors() {
        return this.warriors.sort((one, other) => other.getKills() - one.getKills())
            .slice(0, 10);
    }

/**
* Imperative example
* Describe HOW to do something, mutate's state, not very readable (debatable).
*/
    setWarriorsKills(battleKills) {
        for (let i = 0; i < this.getSize(); i++) {
            const warrior = this.warriors[i];

            for (let j = 0; j < battleKills.length; j++) {
                const kill = battleKills[j];

                if (kill.trim() === warrior.getName()) {
                    warrior.addKill();
                }
            }
        }
    }
}

/**
 * Battle - Responsible for the battle management and keeping the data related to the battle
 */
class Battle {
    constructor() {
        this.duration = getRandomArbitrary({min: 100});
        this.result = '';
        this.kills = 0;
    }

    start(army) {
        let progress = '';
        for (let i = 0; i < this.duration; i += 1) {
            progress += `, ${army.getWarrior(getRandomArbitrary({max: army.getSize()})).getName()}`;
        }

        this.setResult(progress);
        this.setKills(this.result.split(','));
    }

    setResult(result) {
        this.result = result;
    }

    getKills() {
        return this.kills;
    }

    setKills(kills) {
        this.kills = kills;
    }
}

/**
 * Top10Printer - given the top 10 results prints them to the console
 */
class Top10Printer {
    static print(top10) {
        console.log('The top 10 warriors in OOP');
        console.log('-------------------');

        for (let i = 0; i < top10.length; i++) {
            const warrior = top10[i];
            console.log(`${i + 1}. ${warrior.getName()}: ${warrior.getKills()}`)
        }
    }
}

/* ------------------------  Program (Top Level) --------------------------------- */

class War {
    static start() {
        const army = new Army(warriorsNames);
        const battle = new Battle();

        battle.start(army);
        army.setWarriorsKills(battle.getKills());

        Top10Printer.print(army.getTop10Warriors());
    }
}

War.start();

Encapsulation

Encapsulation is the inclusion within a program object of all the resources needed for the object to function — the methods and the data

Take for example the Battle class, it has all the data and logic that it needs to function independently, it knows how to manage itself. This behaviour leads to modularity which in large applications is crucial for maintainability.
Encapsulation prevents access to data except through the object’s functions*, this is important for preventing unintended changes to the instances of the class.

*While this is true for most languages in our case the concept of private does not exist natively in javascript, there are some hacks to mimic this behaviour and there is currently a stage 3 proposal to bring this feature to the language.

Data Abstraction

Abstraction means using simple things to represents complexity.

Abstraction and encapsulation are often confused, and the difference is subtle. Abstraction works at a design level while encapsulation works at an implementation level.
Let’s pick up again the class Battle, we know we can start a battle if we have an army.

const battle = new Battle();

battle.start(army);

We are not worried about how they fight, where they might be or the weapons they use. We don’t tell how it happens it just happens.

Polymorphism

Concept that objects of different types can be accessed through the same interface. Each type can provide its own, independent implementation of this interface.

One of the simplest examples of polymorphism is two classes implementing the same interface. The interface defines that the classes must have some method but leaves the implementation details to each one of them.

Javascript does not have the concept of interface, instead it uses duck typing.
“If it walks like a duck and it quacks like a duck, then it must be a duck”

But we can still get a practical example of this.

// example not included in the codebase

const person = new Character ( name: 'Sheldon');
const warrior = new Warrior ( name: 'Rambo');
const car = [person, warrior];

console.log ('---- Who is on the car - ) ;

car. forEach ( callbackfn: occupant => occupant. showInfo ()) ;

Honestly, I added the showInfo method because when I got to this part I hadn’t any clear example of polymorphism 😅. But this is a nice example both classes instances can call showInfo, on the person or on the warrior, and it works accordingly for each one.

Inheritance

A mechanism where you can to derive a class from another class for a hierarchy of classes that share a set of attributes and methods.

Extends is the keyword here, the class Warrior extends from Character, in this case Character is the parent class and Warrior is the child class. Both share a name property so only the parent needs to manage this property and it will be passed on to his children.
Looking closely the Warrior class implementation only refers to the name property inside the super method which means — I want to inherit the property name from the parent and not worry about that.

Comparison


2 flying dragons

Now that we have seen (and implemented) both approaches and their core principles it’s time to put them side by side and compare them.

While I wrote about how FP is written in a declarative format we can now see the contrast to OOP, which is more imperative, we write how to do, setWarriorsKills (OOP) is a good example, it describes the whole process to accomplish this task.

In OOP approach the army instance contains a collection of warrior instances, when adding a kill to a warrior the warrior changes and this change has a side-effect in the army because that warrior is a part of the warriors, there’s a shared state and instances are mutable unlike the FP approach where nothing changes and everything is created.

The OOP principles that we loop through don’t fit the FP approach, they are very tight to the OOP structure and it is hard to reason about them outside of that structure.

Both produce the same output and run the same scenario, but the OOP version ends up with about the double of lines of code comparing to the FP version. Normally the OOP approaches are more verbose than the FP versions, this example is no exception.

Testing was out of the scope of this article but looking at the code we can perceive that the FP version will be easier to test, much due to being composed by pure functions, while the OOP will be harder to test due to side effects and shared state.

Conclusion

It is not Black & White comparing these two paradigms, it is a lot of grey and many believe that the discussion doesn't even make sense. Probably you already use both on your daily work, even without realising it.
I believe that we should understand the problem/challenge first and then reason about the approach that makes more sense to tackle it.
They are not mutually exclusive, you can use both in the same codebase and get benefits from it.

Javascript was the language used in this article due to being very versatile which makes it easy to use the concepts of both paradigms.

We tend to associate this paradigms to specific languages but Programming languages are becoming evolved enough so that any attempt of categorisation is fuzzy and unpractical.

Regarding languages I encourage to check Scala, created by Martin Odersky. Object-Oriented Meets Functional was at the base of the design of Scala, and I think that most languages are evolving to get the best of both worlds, private properties in classes in Javascript and immutable data structures in Java are just a couple of examples. It is a great time to be a developer 😎.

OOP is more established while FP still looks like a new “trend”, even though it has been around for a few years.
Most of us, at school or at work or while learning online, were more exposed to the OOP paradigm and I believe that, sometimes, to think in FP is a mind shift that is not easy to make and takes time and openness.

Finally, keep in mind that this article provided a light overview about these two programming paradigms, there is a world more to learn about them. I hope it might help, in some way, when discussing this topic over a 🍺 with your colleagues.

Hope you have enjoyed the reading, if you have any suggestion, spotted a mistake or have a different opinion reach me in the comments, I am always eager to learn and hear different opinions.

By the way, what about your battles? 😄

Program result in OOP and FP versions

Inspiration and references

If you enjoyed this article, please 👏 it up and share it so that others can find it! 🙌