Functional vs. Procedural Vs
Functional vs. Procedural Vs
Object-Oriented Programming
Prince Sinha
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:
Object-Oriented Programming (OOP)
Procedural Programming
Functional Programming
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:
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.
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;
}
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.
Reusability: Through classes and objects, and inheritance of common attributes and functions.
Security: Hiding and protecting information through encapsulation.
Maintenance: Easy to make changes without affecting existing objects much.
Inheritance: Easy to import required functionality from libraries and customize them, thanks to inheritance.
Disadvantages:
Java
C++
Python
Lisp
Perl
C#
JavaScript
Lua
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.
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.
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.
Predefined functions: Predefined functions are functions that instead of being in the program, are made available to the developer through an external
library or internally through the programming language itself. Predefined functions are quite commonly found in high-level programming languages. An
example of a predefined function is system.out.println() which allows developers to print items to the console.
Local Variable: These are variables with their scope limited to the block of code in which they are defined. This means that a local variable can only be
accessed within the block in which it is defined, and not from outside. This allows a method to work with its own (local) copy of a variable without affecting
the global state.
Global Variable: Unlike the local variables we discussed above, as the name suggests, global variables are variables that can be accessed from anywhere in
the program. These variables are not bound by any block of code, and though usually defined outside the main function, they can also be initialized from
inside a local scope.
Modularity: Modularity is an important aspect of software development and is essentially a way of separating the functionality into individual modules,
each of which takes up the exclusive responsibility of an elementary task, and works in conjunction with other modules to enable the completion of a larger
compound task.
Disadvantages:
Procedural programming code is not reusable. You’ll have to replicate the code implementation across different programs or files.
Not easy to scale up or extend for larger applications.
Not secure, because of visibility of data across the whole program.
Fortran
Pascal
C
Basic
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).
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.
Pure functions return the same output for a given set of inputs
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
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.
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.
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.
Recursion: Recursion 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.
Reliable: Pure functions will always return the same output for a given set of inputs, and will not have any side effects on the rest of the program.
Transparent: Through an explicit passing of all the parameters that would be involved in the processing of the function’s return value.
Easier debugging: Thanks to immutability, developers need not track the whole history of a variable’s state across the program, and can instead target
variables at specific points in the code where they might be creating problems.
Lazy evaluation: Functional programming allows for lazy evaluation, i.e. values of variables are calculated only when it is required.
Disadvantages:
Using recursion for every iteration operation is unintuitive, as compared to using while and for loops.
Reduced performance because of immutability – especially when having to duplicate large data structures even for making small changes.
Difficult or inefficient to perform recursion without letting variables be updated (immutability).
Though there are only a few pure functional programming languages, here are some which prominently support it:
Haskell
SML
Clojure
Scala
Erlang
Clean
F#
Mathematica
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 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!