In this blog, we are going to talk about Declarative and Imperative approaches in programming. Declarative programming style is about specifying the “What”, while Imperative programming style is about specifying the “How”.
Let’s illustrate declarative programming with a straightforward example. A prime instance that comes to mind is HTML. When creating a basic “hello world” HTML, you articulate the desired appearance of the page. The rendering engine in a browser interprets this specification, generating a Document Object Model or DOM (In short, a tree of objects used by the browser to represent a web page in the form of a hierarchical set of objects) and displaying it on the screen.
<body>
<h1>Hello World!</h1>
</body>
</html>
The above HTML is an example of using a declarative style where the output is declared in the form of an HTML to be interpreted by the rendering engine. In other words, in declarative style, you specify what is required as an output and let the underlying technology (browser rendering engine in this case) or framework execute the details to achieve it.
The above output can also be achieved by using an imperative style using Javascript. Let us take a look at it.
// set the text inside the h1 element to “Hello World!”
var heading_text = document.createTextNode(“Hello World!”);
heading.appendChild(heading_text);
// create a new body element and append the h1 element to it
var body = document.createElement(“body”);
body.appendChild(heading);
// create a new html element and append the body element to it
var html = document.createElement(“html”);
The above is a very simple example and yet we can clearly see that there is a lot more complexity in doing this “imperatively”. We can only imagine the kind of effort it would take if we really had to build complex systems using this approach.
Let’s take another simple example of how an array can be filtered in Javascript using an imperative approach.
const names = [‘Alice’, ‘Bob’, ‘Charlie’, ‘Alex’]
const filteredNames = [];
for (let i=0; i < names.length; i++) {
if (names[i].toLowerCase().startsWith(‘a’)) {
filteredNames.push(names[i]);
}
};
Given below is the equivalent in declarative style
Apart from the obvious difference in the amount of code to be written, there is a more fundamental and important difference. The declarative style only deals with the business logic and not with any of the other details such as how to maintain a variable to hold the result, how to iterate over an array and other lower level details which are not directly related to the business logic.
A declarative programming approach leads to a more maintainable and scalable code. It’s code that is easier to build further upon (scalable) while also being easier to read and debug (maintainable).
When exploring declarative and imperative programming, information often categorizes object-oriented languages as imperative and functional programming languages as declarative, emphasizing their support for higher-order functions (HOFs). While it’s true that HOFs align with declarative principles, it’s essential to recognize that they aren’t the sole means of achieving a declarative style. Even in the absence of HOFs, object-oriented languages like Java can embrace a declarative approach.
Consider Java as an example, especially in scenarios predating SE 8, where lambda expressions were not yet available. In such cases, the java.util.Comparator interface served as a tool for implementing sorting logic. This interface merely necessitates users to define the criteria for determining the greater value when given two values. For instance, let’s explore sorting a list of Person objects in Java based on age, with a secondary criterion of sorting by name if the ages are equal.
import java.util.Comparator
public class PersonComparator implements Comparator {
@Override
public int compare(Person p1, Person p2) {
// Compare ages
int ageDiff = p1.getAge() – p2.getAge();
if (ageDiff != 0) {
return ageDiff;
}
// If ages are equal, compare names
return p1.getName().compareTo(p2.getName());
}
}
people.add(new Person(“Bob”, 25));
people.add(new Person(“Charlie”, 20));
Collections.sort(people, new PersonComparator());
for (Person p : people) {
System.out.println(p.getName() + ” ” + p.getAge());
}
The aforementioned example embodies a declarative approach, as it exclusively involves defining the comparison logic within a ‘compare’ method, with the actual sorting logic managed by the built-in Collections.sort method.
Alternatively, one could opt for an imperative approach by implementing two nested for loops and iterating over each element, employing a bubble sort algorithm with a complexity of O(n^2). While more efficient algorithms could be implemented, the focus of this example lies not in optimizing the sorting algorithm but in emphasizing that, with a declarative approach, the sorting logic itself can be externalized. This separation allows for the utilization of standard sorting algorithms while keeping your own application code cleaner.
Declarative approaches encourage a reuse of tried and tested libraries and frameworks and a clean separation of concerns. The result is simpler, cleaner and more efficient application code which makes it more maintainable and often better in performance!
Some examples of declarative style of programming are listed below
In summary, there are a lot of advantages of using a declarative approach. It promotes DRY (Don’t Repeat Yourself) code, the use of specialised solutions for common problems and consequentially leads to more efficient, maintainable and scalable code.
However, because declarative programming discourages use of lower level details, this logic is often masked behind the interfaces of frameworks. Though this is essential for writing scalable code, it leads to loss of fine grained control, since you would need to work within the rules of the specific framework or library, although there is often a way to work around this situation for exceptions.
To conclude, declarative programming is a style of programming that ensures a cleaner separation of redundant lower level details and the true business logic. It does not belong to a specific programming paradigm and is available in different kinds of programming languages, frameworks and libraries. It can be thought of as a design principle that must be used wherever possible.