What is The Declarative Paradigm?
The Declarative Paradigm is centered on the idea that code should describe what the desired outcome of a program should be, rather than how to obtain the outcome. This contrasts with the imperative paradigm, which centers itself on the programmer writing explicit instructions to achieve a desired result.
More formally, the declarative paradigm focuses on expressing logic without using control flows, i.e., if/else
and loop
statements. Declarative languages often use a style that results in few side-effects, if any. This is usually a natural consequence of not needing to specify explicit steps to solve a problem.
Popular examples of declarative languages are database query languages such as SQL, markup languages like HTML and CSS, and in functional and logic programming languages.
Side Effects
A function is known to cause a side-effect if it modifies a state that exists outside the immediate function scope. For example, changing the global state of your program is considered a side effect, in addition to changing the state of a variable passed by reference. I/O operations to read and write from files are also known as side effects!
Declarative Means "What" (as Opposed to How)
Let's look at a real world example to help clarify what is meant when saying *"declarative programming describes what you want done". Say you're visiting Chicago for the first time, and you want to go to Twin Anchors for some authentic Chicago ribs, see where the bar scene from The Dark Knight was filmed, and maybe even sit in Frank Sinatra's booth #7 if it isn't already taken.
If you were to ask an imperative chicagoan for directions, they might tell you "Oh yeah, you're gonna wanna go three blocks west until you hit Sedgwick, then two blocks north to West Eugenie Street, and it's right there on the corner."
Now if instead you weren't really feeling up for the walk, you could instead grab a declarative driver and say "Take me to Twin Anchors, please". You don't need to know how to get there, because the driver has taken care of that for you. Occasionally, the driver might not take the optimal route, but then again, sometimes they might know some shortcuts that get you there faster!
Code Example
We have a list of names and want to find any name that matches with Amy
. Here's our data:
names = [
{first :'Amy', last: 'Smith'},
{first :'Hank', last: 'Green'},
{first :'Alex', last: 'Armstrong'},
{first :'Sara', last: 'Jackson'},
{first :'Amy', last: 'Graham'}
];
If we take an imperative approach using JavaScript, it would look like this:
let matches = [];
const target = 'Amy';
for(name of names) {
if(name.first === target) { matches.push(name); }
}
return matches;
In this implementation, we're very specific to iterate over every name, check if the name starts with our target, and then add it to the list if it does. These explicit instructions tell us how we want to achieve the result. For many programmers, this is a natural way to want to approach problems.
JavaScript also contains a library of functions that allows us to write declaratively. Using the filter()
command shows us a neat way to achieve the above in one line:
const matches = names.filter(name => name === 'Amy');
return matches;
For lifelong imperative programmers, new programmers, or non-programmers, this might look like a jumble of confusing syntax, so we'll break it down in a way that's more imperative. The filter
method essentially specifies:
for each name in names, keep if name equals 'Amy'
Proponents of the declarative style find that once the syntax is generally understood, it's possible to be more expressive, meaning they feel they can do more while saying less.
Now, instead of doing the searching in JavaScript, we want to do the processing on our server directly, so now we're working with a database containing the same set of data in a table.
Using a declarative language like SQL, we can capture the same logic as our JavaScript counterpart, except in a way where we're specifying what we want as a result, and making no mention about how we want to get to that point:
// "*" means "everything"
SELECT * FROM `names` WHERE `first` = "Amy"
Unlike the filter
method that had a more abstract syntax, the SQL syntax is very straightforward, and you can almost read the code as if it was plain english. Most importantly, though, we can clearly tell that this code is telling what we want the result to look like, rather than how we want to get there.
Subparadigms
The declarative paradigm has two major sub-paradigms; logic programming and functional programming. You may have heard of functional programming with it becoming much more popular in the last decade of computing, however logic programming hasn't necessarily seen the same spotlight (at least, not yet). We'll talk more about functional programming later on since it's well deserving of its own article.
Logic Programming
Now, you might be thinking "I use logic every day when I program, so what!" Logic programming is seated in using first-order logic to treat programs as theorems. Logic programming defines a set of logical facts and rules which are more generally called relations. When writing a program in a logic programming language, you're usually storing the rules in a database, similar to how you might with SQL. When you have your facts and rules set up, you can begin to write queries on the world you've created. A popular logic programming language which we'll use to show logic programming in action is called Prolog.
Facts
Facts are expressions that make declarative statements about one or more things. For example, in Prolog, you could write the following facts to express that I know a certain programming language, and another set of rules for languages I like:
knows(andrew, javascript). /* Andrew knows JavaScript */
knows(andrew, cpp). /* Andrew knows C++ */
knows(andrew, html). /* Andrew knows HTML */
knows(andrew, scala). /* Andrew knows Scala */
likes(andrew, javascript). /* Andrew likes JS */
likes(andrew, html). /* Andrew likes html */
likes(andrew, cpp). /* Andrew likes C++ */
These are now stored in the program as facts that I know these languages.
Rules
Rules are a way of saying "x is y if some statement is true". For example, I am a web developer if I know HTML and Javascript, and I like using the languages. In Prolog, I can type :-
as an operator to express an if statement, and a comma ,
to express an and clause:
web_dev(andrew) :-
knows(andrew, javascript), knows(andrew, html),
likes(andrew, javascript), likes(andrew, html).
/* Andrew is a web dev if he knows javascript and html,
AND if he likes them! */
Queries
When I'm done writing the sets of facts and rules, I can query the database:
/* "Who knows javascript?" */
?- knows(X, javascript)
>> X = andrew /* Andrew knows javascript*/
/* Is andrew a web developer? */
?- web_dev(andrew)
>> yes
These are only very basic deductions, but if you're well versed with your predicate calculus, you can easily define complex relationships, and make powerful deductions. Because of the awesome power of deduction that's built into logic programming languages, it's been used in the past for many AI-related projects like expert systems.
However, due to it's lesser popularity compared to other paradigms, logic programming is e more often regarded as an educational paradigm, however this may one day change!
Bonus: Declarative Code Optimizers
We now know that declarative languages will take care of many of the implementation details for us. However, depending on how critical performance is in your program, it can help to know how the language takes care of certain details.
When I mentioned the cab driver potentially knowing some shortcuts in the story earlier on, it wasn't for nothing; declarative languages often implement some cool math tricks under the hood to help your code execute quickly. In SQL, the query you enter in is rarely the query that's actually being executed; you can view the result of a query optimizer, and not even know that it was taken from your original query.
Optimizations that declarative languages do behind the scenes can even help the code perform in parallel, which has become an increasingly desirable trait; as single cores continue to see less and less relative increases in speed each year, having 4, 8, 12, and even 16 cores is becoming more commonplace. Languages that can naturally exploit many processors behind the scenes will be blazing fast!