Functional vs. Procedural vs. Object-Oriented Programming

Over the decades, different software requirements combined with different sets of programming practices and developer preferences have evolved into a bunch of different paradigms of programming. Each programming paradigm, therefore, presents a different mental framework to logically think about the structure, organization, and data flow of your code.

undefined

To put it simply, a programming paradigm refers to a pattern of programming. It is an idea or a methodology or a set of principles that have been followed for writing software applications and designing programming languages and frameworks.

Most styles of programming or programming language themes can be broadly categorized into three types based on their design, structures, principles, rules, and practices:

All in all, there are more than these three types of programming paradigms, but in this article, we will be learning about the three most common and most popular ones, listed above. Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the guide:

Object-Oriented Programming Systems (OOPs)

Object-Oriented Programming (OOP) is the most popular programming paradigm out there, and usually is the first one beginners are introduced to.

OOP systems allow developers to break down their software into reusable blueprint-like components that dictate a common structure that code entities can adhere to and identify themselves with. This is set in place using classes and objects.

undefined

In object-oriented programming languages, an object refers to an instance or a real entity that follows a blueprint (class). The object is an instance of this blueprint and is used for encapsulating the data and methods that are defined in a class. For example, for a Car as a class, its objects would be actual cars, which will have their own attributes (eg. name, company, model, type, horsepower, etc.) and methods (eg. drive, park, get washed, etc). The class provides a common set of functions for its objects to use, and a bunch of common attributes (placeholders), which then each object can fill to identify itself.

Classes: Using a strict definition of classes, we can say classes are user-defined data types. By user-defined data types, we refer to data types that can be altered and defined according to the needs of the user. Classes are blueprints from which objects can be instantiated. Below is an example of what a class looks like (in Javascript):

class Dog {
    constructor(name, birthday) {
        this.name = name;
        this.birthday = birthday;
    }

    //Declare private variables
    _attendance = 0;

    getAge() {
        //Getter
        return this.calcAge();
    }

    calcAge() {
        //calculate age using today's date and birthday
        return Date.now() - this.birthday;
    }
    
    bark() {
        return console.log("Woof!");
    }

    updateAttendance() {
        //add a day to the dog's attendance days at the petsitters
        this._attendance++;
    }
}

Objects: As we discussed earlier, objects are the real-world entities or instances of their respective classes. Given below is an example object of the Dog class above.

//instantiate a new object of the Dog class, and individual dog named Rufus
const rufus = new Dog("Rufus", "2/1/2017");
Attributes and Methods: Attributes are used for storing the data relevant to each object or each class. They are usually defined in the class’s template and can be updated by the objects during instantiation. The state of an object is defined by the values stored in attributes. For example, in the above Dog class example, `name`, `birthday`, and `_attendance` are attributes. Each Dog object has its own attributes – its birthday and name.

Methods in OOPs are functions defined in the class that can be used by the objects to get things done – to retrieve values, to set values, to manipulate the object’s attributes, or for any other processing. These methods can only be called by the objects to perform various actions. 

Now let’s look at some of the principles of Object-oriented programming that make it stand out amongst other programming paradigms.

OOPS Principles

Polymorphism: In common words, polymorphism refers to the ability of multiple objects sharing the same name, but having different structures or serving different functionalities in different contexts. Polymorphism can be easily observed in function overloading and function overriding.

Inheritance: Inheritance is one of the important concepts in OOPs that allows (child) classes to establish a sense of hierarchy by inheriting the attributes and methods of another (parent) class. This reduces redundancy as classes can share common logic, structure, and attributes while enforcing a clear hierarchy.

Encapsulation: This refers to the wrapping up of the contents of an entity into one unit. In OOPs terms, this refers to the tying up, and wrapping of class or object attributes (state) with their methods (behavior). Thanks to encapsulation, objects can have their own private state which can not be accessed by other objects, unless their methods or attributes are declared public. This aspect of OOP allows for more secure software implementations.

Abstraction: Abstraction in OOPs terms refers to the ability of classes to expose certain data attributes while keeping others private. This is usually done to hide the implementation details from the outside world, either to make things less complex, or more secure. This is done with the help of various access specifiers that specify the visibility of each class attribute.

OOPS Advantages and Disadvantages

Advantages:

Disadvantages:

Languages that follow Object-Oriented Programming

Some of the common OOP languages include:

Now let us move from the paradigm of classes and objects to the paradigm of procedures.

Procedural Programming

In procedural programming, we work with procedures, also known as routines, subroutines, or functions. A procedure is essentially a sequence of instructions or computational steps to be executed. 

Therefore, procedural programming is all about the idea of getting things done in a sequence of steps. This involves thinking about the functioning of your code as a step-by-step course of action that needs to be executed. Here, your code isn’t organized in any logical groups or object-like entities. You just think about the different operations that need to happen in succession and code them down.

undefined

Unlike OOP, where data and methods were tied together (encapsulated) in a class or object, procedural programming uses data and methods as two different entities. As a result, there is no concept of access specifiers here, making this paradigm less secure than OOP.

Here is a basic example of the concept of procedural programming in action:

#include <stdio.h>
void foo(){
   printf(“Arbitrary step 2”);
   printf(“Arbitrary step 3”);
}
int main()
{
   printf(“Arbitrary step 1”);
   foo();
   printf(“Arbitrary step 4”);
   int my_var = 3;
   if(my_var > 0){
      printf(“Complete ✅”);
   } else {
printf(“Error ❌”)
   }
   return 0;
}

You can see how the objective of the program here is to execute a series of sequential steps that I have tried to simulate through print commands here. As you can see, procedural programming can also include functions (from functional programming) and that is not forbidden. This is because the idea here isn’t to absolutely shun functions, or objects, or any other paradigm – those structures serve their own purpose. The idea here instead is for your code to follow a narrative – an order of steps that need to take place for the code to serve its purpose.

Apart from dictating some reusable syntactic structures, a programming paradigm is primarily a way of thinking about programs and how they can be designed. In this regard, procedural programming follows a linear, top-down approach where each program is designed as some combination of a series of code instructions. As opposed to OOP, which resembled the real world in how each entity could be thought of as an instantiation of a specific class (with specific properties and methods), procedural programming lacks such a real-world resemblance in the way entities are created.

Features of Procedural Programming

Some of the key features of procedural programming include:

Advantages and Disadvantages

Advantages:

Disadvantages:

Languages that follow Procedural Programming

Some of the common languages that use procedural programming are:

Functional Programming

Functional programming is all about organizing your code around the idea of using functions. Each function should be set up to perform a clearly defined task and ideally be a pure one (we’ll get to pure and impure functions in a bit).

undefined

Everything in your code, therefore, happens through functions and parameters. You break down the functionality of your code into neat, single-responsibility, reusable functions, and then pass them the necessary data parameters that they need to work with, let them process data (locally, without affecting the global state), and return the required values, which can then be used in the program.

The principles of functional programming are centered around the idea of pure functions. 

Pure Functions

A pure function is one that returns the same output for a given set of inputs, without having any side effects. A common analogy that people refer to is that of a mathematical function, where for a given input x, the output of a function like math.sin(x) will always be the same, regardless of the value of any other variable in your code. And this sine function, under the hood, will not affect the state of the rest of your application in any way. Let us dive deeper into a few characteristics of these pure functions before we take an example to make things more clear. These characteristics are somewhat tied with one another, but we’ll regardless cover them all to get a good overall understanding of the importance of pure functions.

As we discussed above, the output of a method always remains the same for a given set of inputs. This might sound quite straightforward for every function, but it is not. Several functions will use a variable from the global state (for example, for checking a condition), which would then make your code vulnerable to return different values based on the value of an external variable. This external value could very well change because its scope is not limited to the function. Therefore, these are then called impure functions.

Referential transparency is another property of pure functions that states that the invocation of a function (a function call) would very well be replaced by the value it returns, without affecting anything in your code.

This means that pure functions will never modify the input arguments they receive or the global state of the program. This makes pure functions dependable in that they only operate in their own territory without affecting other parts of your code.

Additionally, a function can only be called pure if it does nothing other than calculating the value to be returned. If this function does other things, like making an API request, logging something, interfering with the state of any other object or the global state, it is no longer considered pure.

Along the same lines, pure functions only operate upon the variables that are passed to them through as arguments. This makes their dependencies more explicit and therefore things more clear about the operations of these functions.

Let’s see a very basic example of a pure and impure function to solidify our understanding.

def pure_sum(a, b):
return a + b

def main():
var1 = 25
var2 = 10
var_sum = pure_sum(var1, var2)
print(“Sum is:”, var_sum)

if __name__ == "__main__":
x = 5 # arbitrary variable in your program
main()

As you can imagine, the pure_sum function here is pure – it does only what is expected of it (adding the numbers), only works with the arguments given to it, and doesn’t interfere with any other part of the code. However, if for some reason, this function feels the need to use another variable from the program, then that will be considered impure.

def impure_sum(a, b):
return a + b + x

In the above example, the output of the function now depends on another variable x which is a variable from the global state. Now, you can’t always be sure whether a given set of inputs would always return the same value or not, because x could change – the function has no control over its value.

These principles of pure functions in fundamental programming serve as an extremely useful set of practices that developers can follow for writing clean code across all programming languages, and across all programming paradigms. It promotes the use of functions that are transparent, reusable, and modular – therefore allowing you to write more efficient code. 

Here is a basic example of functional programming in Javascript:

function findElementOnPage(elementTag) {
return document.querySelector(elementTag);
}

function doSomething() {
// do something
}

function connectInput() {
const btn = findElementOnPage('button');
btn.addEventListener('click', doSomething);
}

connectInput();

As you can see here, we have broken up the functionality of our code into multiple functions, where each function serves its own purpose. For example, the findElementOnPage function is a pure function that takes in an argument required for its functionality and does only what it’s supposed to. Even though this function only has one line of code, we chose to have a separate function for providing us with the button element from the DOM. This might seem overkill for an example like this but would turn out to be quite useful if you were to extend these to more complex programs.

Functional Programming Concepts and Terminology

Immutable Data: An immutable variable is one that once initialized, does not change at all. Conversely, a mutable variable is one that can be updated and changed to a different value. 

So when working with functional programming, you should ideally only use immutable data. This means that every time you want to do an operation on a variable, you store the updated value in a new variable instead of modifying the initial one. 

Avoiding Shared State: A shared state, as the name suggests, refers to variables and objects that exist in a shared scope. This means that one variable can be updated from multiple places, making it difficult to track all the updates being made to a variable throughout a program. This is why functional programming suggests against using shared states – restricting variables and objects to their own scope makes managing and debugging code much easier.

First-class and Higher-Order Functions: A first-class function is one that can be used just like any other variable – it can be passed to another function as an argument, returned as a value from another function, stored in data structures, and even be assigned as a value to a variable. Similarly, a higher-order function is one that can take as an argument or return as a value another function. This is how functional programming languages allow you to create and work with functions flexibly.

RecursionRecursion is another common concept attributed to the realms of functional programming, aimed at as an alternative for iteration through while and for loops. Through recursion, each function calls itself repeatedly until a base (like the one used to initiate a while loop) isn’t met.

Advantages and Disadvantages

Advantages:

Disadvantages:

Languages that follow Functional Programming

Though there are only a few pure functional programming languages, here are some which prominently support it:

Which Paradigm is Best?

All three paradigms serve their own purpose, and therefore it wouldn’t be fair to choose one as better over others. Which paradigm works best for you depends on the requirements of your project. 

However, for larger applications, you will usually find it difficult to work with purely procedural programming paradigms. When working with large codebases, code organization becomes supremely important, and therefore you would be better off working with OOP or Functional programming. It would be safe to say that OOP is the more prevalent programming paradigm amongst the three here, and therefore, you should have a good grasp over OOP concepts and principles – because it’s the one you are perhaps going to come across the most. It’s also important to note that quite often, you’ll come across styles of programming that utilize the best of these three worlds, and benefit from the advantages that each of these has to offer. 

Over time, as you gain more experience, you will be able to better appreciate the idiosyncrasies and importance of each of these three paradigms and learn to make the best choice for your application.

Now it’s your turn

In this post, we looked at the three most common programming paradigms out there. We learned about how, more than anything, these paradigms are essentially mental frameworks for developers and organizations to think about how they want to structure their code and which logical organization and flow work best for their application. 

Now go ahead and write some code. But this time, try to observe and think more about the style of programming in the code you read, and in the code that you end up using for your application. Also think about alternatives that you could use, how they would differ in their approach, and whether they would make a better choice.

If you are interested in monitoring the performance of your application for identifying memory issues, bottlenecks, slow database queries, and more, so that you can spend more time building and less time debugging, make sure to check out ScoutAPM!

Happy coding!