DEV Community

Discussion on: Explain Declarative vs Imperative Programming like I'm 5

Collapse
 
luxaritas profile image
Jonathan Romano • Edited

The typical answer you'll get is "declarative programming is what, imperative programming is how". I'll expand on that a little bit: "declarative programming means describing what the end result should be, while imperative programming requires specifying the ordered steps needed to get there".

Benefits you get from declarative programming are largely in making code read more naturally. It allows for your code to visually convey structure and meaning. It allows for the entirety of the intent of some code to be described in one place. It allows a developer to spend less time parsing and instead jump right to the answer of "what does this code do?" It can also mean abstracting away the details of complex APIs behind a cleaner interface, letting someone else deal with the complexity of making sure everything is executed properly.

Admittedly this explanation still isn't great though. It makes a ton of sense if you already understand the concept, but leaves something to be desired as far as understanding how each paradigm is used and why. I think examples are likely the best way to clarify. Excuse me for the web-centric bent here - it's easiest for me to come up with on the spot, though the principles apply in other languages and scenarios.

A great use case is UIs. Compare the following:

<h1>My Shopping List</h1>
<ul>
  <li v-for="item in list">{{item}}</li>
</ul>
function createShoppingList(items) {
  let shoppingList = $("<section><h1>My Shopping List</h1></section>");
  let list = $("<ul></ul>");
  shoppingList.append(list);
  for (const item of items) {
    list.append(`<li>${item}</li>`);
  }
  return shoppingList;
}

The first one uses VueJS templating syntax (for sake of example), while the later uses jQuery. Right off the bat, I hope the first example is easier to read. But why?

  • It goes back to what I described as the benefits of declarative programming. The first example conveys the structure of the UI (the h1 is above the ul, and the lis are within the ul). That's a big one.
  • The latter one requires parsing through syntax - we're assigning variables that we have to remember what they contain, plus enacting side effects in a loop (appending elements to another element), so it's opaque what the end result actually is because we have to include code that handles actually constructing it. What if we wanted to update the content of our shopping list later? Well, either we'd have to rerender the entire section, or we'd do the append operation elsewhere. But then, it means that we don't have one place to look to know where the content is coming from. Imperative programming allows for flexibility (we can arbitrarily change content whenever and wherever we want, anywhere in the UI tree), but that comes at the cost of not being able to go to one single source of truth to determine how our UI is generated.
  • It just plain takes more code to do the same thing!
  • When using a framework (like Vue), it is able to take care of a lot of things behind the scenes that you would otherwise need to do by hand, for example updating the UI when the list variable changes (and it's able to help make sure that it's generally performant with less thought from the developer).

Another example is when considering configuration. You can see this when comparing something like grunt (a task runner, simply executing commands in order) versus webpack (configuration driven, you describe what tools should handle what type of input, how you want the output formatted, what project-wide conventions to use for things like path aliases, etc). Another example would be an Infrastructure as Code tool like Terraform, where you describe, say, the type of servers you want and what their settings should be in a config file rather than making calls to an API. I can't immediately come up with a good concrete code example for this one, but a big benefit here is that often it means that you can say "here's what I want my system/process to produce, you go figure out how to get everything set up" without having to handle the details of doing it efficiently (eg with concurrency), disparate or complex APIs from various providers, or just generally doing as much work by hand.

A final interesting situation is functional programming. While I'm not personally a proponent of writing fully functional code, I do take advantage of many of FP's concepts because of their declarative nature. I do appreciate that functional programming is super intimidating if you haven't seen it before (it was for me!), and there are benefits to being explicit over terse. That said, once you get a grasp of it, there are great readability benefits because of this declarative nature. Take for example (evidently I haven't gotten to the refrigerated items yet):

// Imperative
import storeAPI from 'some-store-api';

const shoppingList = [
  {name: "bread", bought: true},
  {name: "eggs", bought: false},
  {name: "milk", bought: false},
  {name: "brownie mix", bought: true}
];

let itemsToBuy = [];
for (const item of shoppingList) {
  if (!item.bought) itemsToBuy.push(item);
}

let totalCost = 0;
for (const item of shoppingList) {
  totalCost += storeAPI.getPrice(item.name);
}
// Declarative, (more) functional
import storeAPI from 'some-store-api';

const shoppingList = [
  {name: "bread", bought: true},
  {name: "eggs", bought: false},
  {name: "milk", bought: false},
  {name: "brownie mix", bought: true}
];

const itemsToBuy = shoppingList.filter(item => !item.bought);

const totalCost = shoppingList.map(
  item => storeAPI.getPrice(item.name)
).reduce(
  (total, itemPrice) => total + itemPrice
);

Some distinct benefits of the latter approach:

  • You can read the code more like natural language. For example, let itemsToBuy = shoppingList.filter(item => !item.bought); can be read as "itemsToBuy is (=) the items of shoppingList (.filter) where the item hasn't been bought (item => !item.bought)". With the first approach, you can technically still do that, but it's not as natural - especially as with the second approach, the evaluation of the expression is directly assigned to the variable (as opposed to using an explicit "accumulator").
  • There's less code to read, and less going on. As I said, there's benefits to being explicit over terse. That said, with less code to read, there's less to parse, less to keep in your head, and less to track. With the first approach, you have to consider the accumulator, the iteration, the side effects, etc. In this example it's not a huge difference (especially thanks to the for..of removing the need to keep track of an array index too!), but there are situations where it can be a help.
  • You have the ability to avoid side effects. In the latter example, everything can use const. The benefit here? The contents of the variable get declared exactly once. You don't have to go tracking down where it gets changed if you decide to adjust it later (or do it on accident), and it prevents you from reusing variables once their meaning changes.
  • Interestingly, even though declarative is "what not how", it can actually make the individual "steps" clearer. With totalCost, you first take the price of each item (the map) and then add them together (the reduce). This example might honestly be clearer with the imperative approach, but when you have a number of steps to get from point a to point b, it can be nice to have the explicit steps of translating your array from one state/meaning to another - where you tend to get into trouble is when steps get nested instead of chained. This is where I find the FP approach becomes quite opaque and doing it more imperatively can be clearer. Another approach is to split out the internal steps to individual assignments to separate variables.

Hopefully this was a useful way of looking at it, if there's anything still left confusing I'd be happy to clarify. 🙂

Collapse
 
khophi profile image
KhoPhi

This is an interesting read and exhaustive. Thanks for sharing

When using a framework (like Vue), it is able to take care of a lot of things behind the scenes that you would otherwise need to do by hand, for example updating the UI when the list variable changes (and it's able to help make sure that it's generally performant with less thought from the developer).

I'm not sure how to understand this part. Does a framework cutting down on what I need to write in a way making me write "declarative programming" i.e "declarative programming means describing what the end result should be"? Because in the example of the UI between vue and jquery, the Vue abstracts the rendering of the template away, whereas the Jquery approach, one has to write it out.

In other words, behinds the scenes, the Vue abstraction that got the rendering of the li to happen would have be declarative or imperative?

Maybe I'm confused.

Collapse
 
deciduously profile image
Ben Lovy

If I'm understanding your question correctly, yes, Vue's internals use imperative code so that consumers of the tool are able to stick to declarative code. This is much like a database engine - the operations required to execute your SQL query are calculated by the database engine and invisible to the user, but will naturally be implemented inside the engine in an imperative fashion. Somewhere in the pipeline you will always need to imperatively tell the computer what to do.