Imperative versus declarative programming

Imperative programming concerns itself with how something is accomplished, while declarative programming concerns itself with what we want accomplished. It's difficult to see the difference between these so it's best to illustrate them with a simple program:

function getUnpaidInvoices(invoiceProvider) {
const unpaidInvoices = [];
const invoices = invoiceProvider.getInvoices();
for (var i = 0; i < invoices.length; i++) {
if (!invoices[i].isPaid) {
unpaidInvoices.push(invoices[i]);
}
}
return unpaidInvoices;
}

This function's problem domain would be: getting unpaid invoices. That is the task the function has and it is what we want to achieve within the function. This particular function, however, concerns itself a lot with how to achieve its task:

  • It initializes an empty array
  • It initializes a counter
  • It checks that counter (multiple times)
  • It increments that counter (multiple times)

These and other elements of our function are not at all related to the problem domain of getting unpaid invoices. Instead, they are the rather annoying implementation details that we must go through to get our desired output. Functions like this are called imperative because they are mostly concerned with how.

While the imperative form of programming busies itself with procedural low-level steps involved in a task, the declarative form of programming uses abstractions to avoid the use of direct control flow, preferring to express things only in terms of the problem domain itself. The following is a more declarative version of our getUnpaidInvoices function:

function getUnpaidInvoices(invoiceProvider) {
return invoiceProvider.getInvoices().filter(invoice => {
return !invoice.isPaid;
});
}

Here, we are delegating to Array#filter so it handles the specifics of initializing a new array, iteration, and conditional checking. We have freed ourselves from the complexity of conventional control flow by using an abstraction. 

Declarative patterns such as this have become the staple of modern JavaScript. They allow you to express the logic you desire at the level of your problem domain, instead of having to worry about lower layers of abstraction such as how to iterate. It's important to see that both declarative and imperative approaches are not completely distinct. They are at either end of a spectrum. On the declarative side of the spectrum, you are operating at a higher level of abstraction, and are hence not exposed to the implementation details that you would be without such abstraction. On the imperative side of the spectrum, you are operating at a lower level of abstraction, utilizing lower-level imperative constructs to tell the machine what you want to accomplish:

Both of these approaches have implications for our control flow. The more imperative approach directly states that it will iterate once through the array and then conditionally push to the output array. The more declarative approach does not make any demands about how the array is iterated through. Naturally, of course, we know that the native Array#filter and Array#map methods will independently iterate through their input arrays, but that is not something we are specifying. All we are specifying is the condition on which our data should be filtered and mapped. How the data is iterated through is completely the concern of the Array#filter and Array#map abstractions.

The benefit of a more declarative approach is that it can increase clarity for the human reader and enable you to more efficiently model complex problem domains. Since you're not having to worry about how things are occurring, your mind is left free to purely concern itself with what you wish to achieve. 

Imagine we're given of task of conditionally executing a specific piece of code but only if a certain feature is enabled. In our mind, this is how it should work:

if (feature.isEnabled) {
// Do the task.
}

This is the code we want to write, but we later find out that things are not so simple. For starters, there is no isEnabled property for us to use on the feature object. There is, however, a flags array property, which when fully disabled will include Feature.DISABLED_FLAG:

// A feature that is disabled:
feature.flags; // => [Feature.DISABLED_FLAG]

That seems simple enough. But then we discover that, even if the feature does not have this flag and so seems enabled, we also need to check that the time right now aligns with a set of times stored in feature.enabledTimeSlots. If the current time is not in one of the enabled time slots, then we must conclude that the feature is disabled, regardless of whether it has the flag.

This is starting to become quite complicated. In addition to checking for the disabled flag, we'll need to go through these time slots to discover whether the feature is currently enabled based on the current time. So, our simple if statement has very quickly become an unwieldy mess, with several layers of control flow:

let featureIsEnabled = true;

for (let i = 0; i < feature.flags.length; i++) {
if (feature.flags[i] === Feature.DISABLED_FLAG) {
featureIsEnabled = false;
break;
}
}

if (!featureIsEnabled) {
for (let i = 0; i < feature.enabledTimeSlots.length; i++) {
if (feature.enabledTimeSlots[i].isNow()) {
featureIsEnabled = true;
break;
}
}
}

if (featureIsEnabled) {
// Do the task.
}

This is undesirably complex code. It's very far away from the original declarative code we wanted to write. To understand this code, a fellow programmer will have to maintain the state of featureIsEnabled in their head while scanning through each of the individual constructs. This is a mentally burdensome piece of code to navigate through and is, therefore, more liable to misunderstandings, bugs, and general unreliability.

The key question we must now ask ourselves is the following: what would it take for us to abstract away all of these nested layers of control flow away so that we can have our simple if statement back?

We eventually decide to place all of this logic in a newly created isEnabled method within the Feature class—but not only that! We decide to abstract the logic further, by delegating to two internal methods, _hasDisabledFlag  and _isEnabledTimeSlotNow. And these methods themselves delegate their iteration logic to array methods, includes(...) and filter(...):

class Feature {
// (Other methods of the Feature class here,..)

_hasDisabledFlag() {
return this.flags.includes(Feature.DISABLED_FLAG);
}

_isEnabledTimeSlotNow() {
return this.enabledTimeSlots.filter(ts => ts.isNow()).length;
}

isEnabled() {
return !this._isDisabledFlag() && this._isEnabledTimeSlotNow();
}
}

These very small declarative additions to the Feature class enable us to write the declarative code we were originally aiming for:

if (feature.isEnabled()) {
// Do the task.
}

This has not only been an exercise in simple abstraction. This has been an exercise in reducing the layers of control flow. We've avoided the need to use nested layers of for if and for blocks, reducing the cognitive burden faced by ourselves and our fellow programmers, and fulfilling the task we originally set out to accomplish in the cleanest way possible.

By carefully refactoring and abstracting our original mess of control flow we have, quite oddly, ended up with a set of code that includes very few traditional control flow statements (if, for, switch, and so on). This doesn't mean our code is without control flow; rather, it means that the control flow is either minimized or hidden away under layers of abstractions. When using the native control flow constructs of the JavaScript language, it is important to remember that they are not your only tool with which to express the flow of a program; you can redirect and split complicated logic into abstractions that each handle a very specific part of your program's flow. 

Now that we've got a solid foundational understanding of what control flow is and how it melds with what we know about abstractions, we can go through each of JavaScript's individual control flow mechanisms, highlighting challenges and potential gotchas.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.218.69