What the Heck is Declarative Programming, Anyways?
Keith Brewster
Posted on August 13, 2019
Chances are—at some point in time—you've heard someone bring up the concept of declarative programming. Maybe it was in a Medium article, or maybe you saw someone mention it on Twitter. Perhaps you were hanging out at a local tech social, when all of a sudden the brilliant, psychopathic CTO of some shady startup real estate disruptor started smashing empty beer bottles on the bar, brandishing the crude glass weapon & threatening to slash everybody in the room if they didn't stop using if/else statements.
They probably looked something like this.
"Declarative programming?" you ponder to yourself, "perhaps Wikipedia is able to summarize it in a simple & digestible manner for all of the newbies interested in doing some light research on the subject." Except you don't ask yourself that, because you know that reading anything technical on Wikipedia leaves you with the kind of headache rivalled only by the hangover after a twelve hour binge of the cheapest malt poison available at your local liquor store. The articles you come across are all different flavours of the same pain. One long-winded term eventually leads to the next, until it becomes a never-ending rabbit hole of self-destructive internet sleuthing, and by the time you're on the other side you can't even recognize yourself in the mirror anymore.
Actual photo of my research for this article.
Okay... so that could have been hyperbole, but hopefully I can ease the pain a little. A lot of people will argue the semantics of what can be considered truly declarative; I'm not writing my PhD thesis, so we're going to be learning the fun way (if you want the PhD version, please see this StackOverflow answer).
If you've ever looked up what declarative programming is, you'll probably be well-acquainted with some variation of this common answer:
Declarative programming is describing the WHAT and imperative programming is describing the HOW.
Alright, but what does that mean? There's a few things to unpack first: declarative programming has an antithesis known as imperative programming. You'll almost always find comparisons of these two opposing paradigms. But here's the thing, while these two approaches are opposites in execution, it doesn't mean they don't co-exist. This brings me to my first lesson:
Lesson 1: Declarative Programming Can't Exist Without An Imperative Abstraction (It's Just Layers)
I know I said this would be a beginner-friendly guide, so let me simplify what I mean by this. My work has this weird, fancy coffee machine with two pages of different coffees that it's capable of brewing, in which you will only ever drink exactly two of them.
The second page has an option for "vanilla choco latte", but nobody is brave enough to discover where these flavours come from.
Think about using this ungodly contraption vs. a French press. Let's say you're feeling particularly risk averse, and decide that you're going to stick with regular ol' coffee. You approach the monolithic coffee dispensing monstrosity and click "Pilot Monument". The machine makes a startlingly violent chugging noise, and the coffee dispenses into your cup. You don't really need to be concerned about what's happening between when you press the button and when it you get your coffee—you just get the drink you asked for. The coffee machine is a rough example of declarative programming. The implementation details are hidden; you express what you want, you don't specify how it should be done. Lets look at the imperative approach with the French press:
- Pick your beans and grind them.
- Boil water in a kettle.
- Remove the plunger from the French Press, and pour your coffee grinds in.
- Pour the boiling water into the French Press.
- After 3-4 minutes (or desired steeping time), press the plunger down slowly to separate the grinds from the water.
- Pour the result into a mug to enjoy.
There's a clearly defined control flow to follow; each step of the process is clearly laid out and executed. It's all good and gravy to tell an application what you want it to do, but something still needs to be pulling these levers behind the scenes!
Here's the same concept applied in a more practical setting. You may be familiar with the higher order function map
added with ES6. If you're not familiar, let me summarize it quickly: map
is a property of the JavaScript Array
object that will iterate over the array that it's called on, and execute a callback on each item. It returns a new instance of an array; no modifications are made to the original object. Let's take a look at a comparison of functions (both declarative and imperative) that map over an array of strings and append the octopus '🐙' emoji on the end of each one (objectively the best emoji).
// Declarative
const addOctopusEmoji = arr => arr.map(str => str + "🐙");
// Imperative
const addOctopusEmoji = arr => {
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] + "🐙"
}
return arr;
}
Fairly straightforward, and a good demonstration of this first lesson. map
is a much more declarative approach than the traditional looping mechanism. You're not programming the control flow that determines how to iterate over every index of the array and apply the necessary operation. map
does this heavy lifting for you. But map
has an imperative abstraction. It's not magic, it needs to be doing something under the hood. The difference is that you don't need to be concerned with the implementation details of how it goes about doing its business (and as a bonus, it returns you a new instance of an array. This means you're not mutating any existing references like in the imperative example & causing any unintended side effects; more on this later). It's just layers, friends! Alright, now you're one step closer to being a declarative programming champ.
Basically you right now.
Lesson 2: Declarative Programming Is Not Functional Programming
That's not to say they're completely different ideas. A lot of people consider functional programming a subset of declarative programming. A true declarative program is written as an expression that gets executed/evaluated, with the ability to specify what you want the result to be (again, going back to that description you read everywhere). A good example of a declarative language is SQL.
SELECT
*
FROM
tough_guys
WHERE
name = 'Keith Brewster'
Query returned 0 results.
Sorry to every other Keith Brewster out there, don't act like I haven't seen you while Googling myself.
You're not in charge of manually parsing the name column of a table and finding every tough guy named 'Keith Brewster'. You provide the constraints in the form of an expression, and SQL returns what you asked for. Thanks, SQL.
Now let's look at JavaScript. You can't just slap a single expression into an application and expect the JavaScript engine to run everything for you. You have to build out the functionality of your application with a series of functions (see where I'm going, here?). This doesn't inherently make JavaScript a functional programming language, because FP comes with its own set of rules and constraints. You can—however—apply these concepts in your code and use JavaScript like an FP language, just the same as how you could use classes and inheritance in JavaScript and operate like an OOP language. It's just another way of building out your application architecture.
What architecture should we build this house with, boss?
Functional programming is considered a subset of declarative programming because it also seeks to avoid writing code in an imperative or procedural way. I'm not going to dig too much into FP here (maybe it's foreshadowing for a future article). All you really need to know at this point is that declarative is not functional, but functional is declarative.
Lesson 3: A Decent Amount Of Modern Frameworks Handle UI Declaratively
Side story: in college I was constantly entrenched in Java. Every semester we just did more and more Java. Sometimes we touched other languages (C++, C#, PHP), but most of the time we were just building variations of calculators or solving math problems that we already covered in Java. Needless to say, it came as quite the shock when I left school and found that the job market was not 95% Java, despite my education preparing me for such a reality. I hadn't picked up much of an interest for web development in college, but I quickly fell into it after graduating. Getting thrown into JavaScript was a huge change for me; I started seeing people write code in different, exciting ways. If I can make one recommendation in this article, it's to open yourself up to different perspectives. Seeing how other people approach problems has been instrumental for me in growing as a developer.
Anyways, back on track. What is declarative UI? It's just another abstraction, but instead of hiding the implementation details of a function, we're hiding the implementation details of changing the UI—stick with me here. Let's take a look at how React takes a declarative approach to UI:
<PotentiallyGreenButton
handleClick={toggleIsButtonGreen}
buttonGreen={isGreen}
>
{buttonText}
</PotentiallyGreenButton>
So here we have our PotentiallyGreenButton. It's a button that could be green, or maybe it's not green. We'll never know. Back in the day, if you wanted to update a DOM element you'd need to create a reference to it, and apply your changes directly to the element. That's a major inconvenience; your functionality is coupled to that single element (or depending on how you target elements, all of them). React abstracts updates to the DOM so you don't have to manage it. You're only concerned with developing your components—you're not in charge of the implementation details of how the DOM elements are updated during every render cycle. You also don't need to concern yourself with managing DOM event listeners. React provides you a library of easy to use SyntheticEvents that abstract all of the DOM event logic away so that you can focus on your important business logic (in this case, the green-ness of your maybe green button).
Lesson 4: In The End, There's No Right Or Wrong Way
I love approaching my code in a declarative way. Maybe you don't, maybe you like explicitly stating your control flow. Maybe it's just easier for you to understand, or just comes more naturally to you. That's totally cool! It doesn't make you any less valuable as a programmer, so don't feel bad if you're not used to it (and don't let anybody else tell you otherwise). The most important thing is being able to understand the ideas behind the methodologies of different approaches. You do you!
Before we wrap up, I just wanted to highlight a few reasons I love taking a declarative approach to coding:
Context-Independent:
A more declarative style allows you a greater degree of modularity. If your functionality isn't coupled to any sort of application state, it becomes context-independent. You can reuse the same code in any application, and it should function the exact same way. This means you should avoid changing any data that lives outside of the context of your function (global variables, etc).
Readability
This might be a hot take, but I think a declarative approach is more readable, as long as you put in the effort to have self-documenting function/variable names. Some people might find it easier to look at a control flow (loops, if/else statements) and follow along with each step, so this is more of a subjective benefit.
No Side Effects
Hey remember that little blurb of text inside the parenthesis back in my first point saying "more about this later"? Well, we're here! A side effect is what happens when modifying a value in one area causes an unintended effect somewhere else in the application. In a declarative application you should treat everything as immutable. That means that after you've initialized a variable, it can't be modified. If you want to update a value, you should initialize a new variable based on the item with any of the modifications you want to make (much like we did in our octopus '🐙' example with array.map). If you're not mutating application state, it shouldn't cause a side effect anywhere else in your app.
It's Fun!
Taking on a new approach to how you code is a fun challenge, and you may find yourself discovering new ways to approach problems. Because you stop relying on loops, you work more with recursion. Trying to reduce reliance on if/else statements might lead you down the path of functors. It's good practice, at the least!
It's finally over, you've made it.
Whew! Thanks for sticking with me this far, I know there was a lot of content to digest. If you enjoy what I do, consider following me on Twitter. I hope I've been able to help you out a little today!
Cheers!
Posted on August 13, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.