Programming paradigms are models employed for developing computational solutions to problems. These paradigms establish the rules and logic to develop programs. In this way, programming languages implement these rules and logic, providing them for programmers. Currently, most existing programming languages support the imperative paradigm, the declarative paradigm, or both.
In this article, we’ll explore the imperative and declarative programming paradigms. First, we’ll have a brief review of programming paradigms. Then, we’ll investigate the imperative paradigm. In this context, we’ll analyze the derived paradigms of procedural and object-oriented programming. Next, we’ll inspect the declarative paradigm and its derived paradigms of functional and logic programming. Finally, we’ll review and compare the programming paradigms into a systematic summary.
2. Outlining Programming Paradigms
As a result of computer programming, human beings could improve their performance in executing several processes. There are various methods to program computers. However, regardless of the adopted method, the programmer must communicate with the computers through a specific language.
This language can be machine language, which considers only binary symbols (0 and 1). But, writing programs using only binary symbols is pretty unusual and difficult for modern humans that employ complex and rich language systems. So, in order to overcome these difficulties, many programming languages with particular lexicon, syntax, and semantics were proposed over the years.
The programming languages subdivide into paradigms according to their features. Examples of these features are the code structures (how the programmers should write the code) and the execution models (how the computers proceed with the program execution). Currently, there exist two major programming paradigms: imperative and declarative. These paradigms, in turn, subdivide into other paradigms with even more particular characteristics.
In the following sections, we’ll explore the main characteristics of the most prominent programming paradigms and understand how they work.
3. Imperative Paradigm
The imperative paradigm is the oldest computer programming paradigm. This paradigm presents as a central characteristic the definition of sequences of instructions representing modifications in the states of a computer system. Thus, we can see programs following the imperative paradigm as “guides” that describe how to accomplish some task.
The imperative paradigm has several references in von Neumann’s architecture. This architecture takes into account the use of a processing unit, a control unit, and memory. In summary, the processing unit consists of an arithmetic module and processor registers, and the control unit has instruction registers and a program counter. The memory, in turn, keeps data and instructions.
Imperative programming languages mimes strategies presented in the context of von Neumann’s architecture. For example, the memory stores data and instructions of a program. Thus, statements are employed to manipulate the data in the memory, such as assigning it to variables or executing arithmetic operations. There is a sequential order for executing instructions of programs. So, a special variable, the instruction pointer, indicates the next program instruction to execute. At last, conditionals, loops, and other control commands, such as goto, can modify programs’ execution order, changing the value of the instruction pointer.
The imperative paradigm has two popular derived paradigms: procedural and object-oriented. In the following subsections, we’ll see some relevant concepts about them.
3.1. Procedural Programming
The procedural programming paradigm focuses on subdividing a program from a simple sequence of instructions to a collection of subroutines with particular instructions, structures, and variables. Prior to procedural programming, the repetition of a specific portion of code relied on using goto statements. However, it made program codes very complex to follow and hard to maintain and update. Thus, the partition of a monolithic code into subroutines promoted by procedural programming significantly improved the modularity of program codes.
A subroutine of a program is tailored to accomplish a single well-defined task. However, implementing subroutines demanded new instructions for developing programs:
- Declaration: the declaration of a subroutine typically defines its name and the required parameters. In most languages, the subroutine definition (the code) is presented just after the declaration
- Call: instruction that triggers the execution of an already declared subroutine. Thus, the data corresponding to each parameter of a subroutine is defined within the calling instruction
- Return: the return instruction indicates the result of a subroutine. However, some subroutines do not present returnable results and, consequently, do not execute the return instruction
The following image presents an example of non-procedural and procedural equivalent programs to sum or subtract two numbers:
Examples of programming languages that support procedural programming are Basic, C, Pascal, and Python.
3.2. Object-Oriented Programming
After the procedural programming paradigm, the imperative paradigm got two sequential evolutions: structured programming and modular programming. First, structured programming defined that a subroutine has a single starting command and, preferably, a single exit point. So, modular programming divided programs into modules with particular members: names, procedures, variables, and submodules.
The object-oriented programming paradigm was inspired by concepts from modular programming. However, the object-oriented paradigm aims to represent the real world, modeling it similarly to our brains. So, this paradigm leads new meanings and properties to concepts from modular programming, remodeling them. Examples of these remodeled concepts are classes, attributes, and methods:
- Classes: representations of things in the real world. Classes have a name and typically have several attributes and methods. Furthermore, different from modules that are imported only once in the same code context, we can instantiate classes multiple times as objects
- Attributes: information stored in the context of a class. Thus, attributes represent the states of a class object. Attributes have some similarities with variables of modules. However, objects have particular namespaces. So, two objects of the same class can have different values for the same attribute
- Methods: in summary, a method is a procedure associated with a class. Methods are called on the context of a class object. Thus, methods can produce internal changes, such as modifying an attribute value, and other side-effects, such as communicating over the network
The following image highlights the previously presented concepts in a concrete example:
Examples of programming languages that support object-oriented programming are C++, PHP, Java, and Python.
4. Declarative Paradigm
The declarative paradigm has as the main feature the definition of tasks that computer systems must accomplish. Different from the imperative paradigm, the declarative paradigm doesn’t make clear how to process a task. On the contrary, programs following the declarative paradigm work as “guides” with instructions defining what tasks a computer system should accomplish, regardless of how these tasks are accomplished.
Due to this characteristic of describing what to do instead of how to do something, program codes in the declarative paradigm naturally pose a higher abstraction level. It is a consequence of using predetermined command structures to define tasks. This higher abstraction level also improves the maintainability of a program due to the standardized operation of these command structures. Thus, it is easier for different teams to work on distinct stages of a software lifecycle, such as development and maintenance.
Two popular paradigms derive from the declarative paradigm: functional and logic. We’ll study these paradigms in the following subsections.
4.1. Functional Programming
The functional paradigm considers that programs are compositions of multiple functions. In such a scenario, we can employ functions as any data type. Thus, functions can have identifiers and work as arguments or results of other functions. Furthermore, there are different categories of functions, each one considering particular characteristics:
- First-class functions: elementary functions for the functional paradigm. They can be assigned to variables, passed as arguments to other functions, and returned as other functions result
- Pure functions: these functions follow the properties of transparency and lack of side effects. Transparency signifies that a function always returns the same result given the same input. Lack of side effects means that a function doesn’t cause any state mutation in the program, for example, printing some information or sending data through the network
- High-order functions: these functions receive other functions as arguments or return a function as a result (or even both). An example is the map function, which executes a provided function to each element of a given iterable, thus returning a new iterable
The following image shows a practical example of the described functions:
Finally, an interesting aspect of the functional paradigm is that variables aren’t appreciated. In this way, data should never change (immutability property). So, for example, to add a new value to a list, we should destroy the old list and create a new one with the same values plus the new value.
Examples of programming languages that support functional programming are Haskell, Scala, and Kotlin.
4.2. Logic Programming
The logic programming paradigm uses formal logic to solve a myriad of problems. This paradigm relies on a knowledge base with several facts and rules to answer queries:
- Facts: facts represent specific relations among objects. Concretely, facts are assertions about objects
- Rules: they are predicates that describe a logical relation among facts. In practice, rules have head and body, where generally head is an assertion, and the body is a condition
- Queries: a query consists of one or more logic expressions interleaved with logic operators. Furthermore, to have a proper answer, we should create queries considering the facts and rules into the programs’ knowledge base
With the described resources, programs use an inference system to answer queries. So, the inference system searches for proofs for each query. Finally, the program returns the deductible solutions to a query. Otherwise, if there are no solutions, the program returns a false value.
The following image demonstrates the previously presented concepts:
Examples of programming languages that support logic programming are ASP, Datalog, and Prolog.
5. Systematic Summary
The imperative and declarative paradigms have differences between them. Making an analogy, consider that your car has broken. So, you can follow a step-by-step tutorial to fix it by yourself (imperative solution) or ask a mechanic to solve the problem (declarative solution). Without extra information about the context of the problem, it is not possible to say which alternative is better. The same occurs for programming paradigms.
The imperative and declarative paradigms, however, have comparable features, advantages, and disadvantages. The following table summarizes some of them:
In this article, we learned about imperative and declarative programming paradigms. First, we studied the imperative paradigm, defining its main characteristics and investigating the derived paradigms of procedural and object-oriented programming. So, we similarly studied the declarative paradigm, especially investigating the derived paradigms of functional and logic programming. Finally, we put both the imperative and declarative paradigms face to face into a systematic summary.
We can conclude that the existence of different programming paradigms is essential for developing better programs. In this way, programmers can choose their strategies and tools that best fit the problems to be solved.