Computer Science Notes

Programming Paradigms

A Precursor About Imperative vs Declarative

This should serve as a short and easy descriptor to both Imperative and Declarative Programming ideologies, but I do still recommend reading the more in-depth versions as they give information about more specific aspects on top of this general description. Ultimately, Imperative Programming is defined as telling a computer every step of the process to execute in very specific terms. On the other hand, Declarative Programming is defined as telling a computer what we want to have happen in order to make the computer do things we want without necessarily defining each step. However, there is a large caveat to Declarative Programming in that it is actually all just an abstraction of Imperative Programming. For example, looking for keys Imperatively would be written as “look under the mattress, then on the couch, then in the bed, then …” whereas looking for keys Declaratively would just be “look for keys.” Either way the same steps may be done, but Declarative programming separates the defined execution from the desired logic. As such, Declarative programs are just Imperative code hidden behind a language itself, as there is no other way to do so. This video is great to watch to understand.

Imperative Programming and Structured Programming

As said above, Imperative Programming is the act of defining every step for the computer to execute in a sequential order. In Imperative Programming, the order of execution matters heavily as there is a large reliance on data state and expressions, which are known as statements in Imperative Programming as they modify data's state in some way. As an example, Pure Assembly Language, which is very low-level programming that depends on the computer architecture's available instruction set, is a great example of Imperative Programming, as every single step that the computer executes is defined clearly. Essentially, code would be written in one big chunk of statements that mutate the state of the program's data in order to achieve the desired result.

A purely Imperative Programming language is extremely bare-bones and really is just simply a “do this, then this, then this, and so on” form of writing code. As such, there was a large possibility for improving the quality of life aspects of Imperative languages. Given this idea, Imperative Programming was later extended by the concepts of Structured Programming, which sought to improve the flow of purely Imperative languages. Structured Programming is essentially exactly the same as Imperative Programming, but adds various ideas such as if/then/else statements, code blocking, and subroutines (functions). I include Structured Programming here because it is essentially just pure Imperative code but with some quality of life changes. This is the essence of languages with different paradigms: all are extensions of Imperative code in some form or another.

Declarative Programming

I would like to start by building on the common definition of Declarative programming given previously: Declarative programming is telling a computer what we want to have happen rather than how to do it. I stated previously that Declarative Programming is just an abstraction of Imperative programming, and so the computer in this situation is actually the language itself which has some built-in implementation of some actions. Take, for example, the following code written in C#:


int count = 0;
int[] vals = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
    if (vals[i] <= 3) {
        count++;
    }
}
int[] underFourDoubled = new int[count];
int cursor = 0;
for (int j = 0; j < 5; j++) {
    if (vals[j] <= 3) {
        underFourDoubled[cursor] = vals[j] * 2;
        cursor++;
    }
}
Imperative

int[] vals = {1, 2, 3, 4, 5};
vals = vals.Where(vals => vals <= 3).Select(vals => vals * 2).ToArray();
Declarative
In this code, we begin with vals equaling {1, 2, 3, 4, 5}, but then take every value that is less than 4 and double it, only adding those values to a new array that we then set vals equal to. In the imperative method, I have to parse to find the number of values less than 4, create a new array of that size, then parse them again to add their doubled values to the new array. Contrastingly, the Declarative approach uses the data querying methods included in C#'s LINQ framework, specifically Where() and Select() .

Where() filters the passed in input stream that can be iterated through and adds the values to a new list dependent on if they get returned by the passed lambda expression. Select() takes an iterable input stream and applies the passed lambda expression to that data, returning a list of those changed values. These methods are built in to C# to make data-querying easier to write as they are often-done procedures. There isn't necessarily anything special about how these methods are done, they are all written imperatively, but coding using functions that hide the implementation is what makes it declarative code.

In terms of benefit to programming it this way: the main benefit of writing Declarative code whenever possible is that it allows for faster programming, debugging, and is more safe to program as the way the computer interacts with the queried function is consistent. There is also a potential for having some efficiency benefits in the form of parallel processing, but the majority of cases will be slightly more inefficient, which is really not a problem unless having very efficient code is super necessary, like when the code is being repeated over and over and over in a very short span of time.

You may have recognized by now that Declarative programming is pretty much only applicable when functions have already been defined and, due to this, not all code can be written Declaratively when only using libraries, frameworks, or language features written by others. However, as Declarative programming is essentially just done by using a clearly-defined function, or one that essentially self-comments, Declarative code can be done by abstracting a certain type of function. The functions that define a Declarative function differ from typical Imperative coding functions in the way that they must be Referentially Transparent.

Referential Transparency and Pure Functions

Referential Transparency (RT), which isn't necessary when creating functions in Imperative programming, is the defining feature that allows Declarative programs to be able to exist. In essence, Referential Transparency is the term used to describe expressions which logically allow for the expected value to be substituted in the place of the expression itself. In other words, putting an input into the function would be effectively the same as just writing the output. So, for example, passing 5 to a referentially transparent function that returns 2 * n, where n is the passed number, would be the same as just writing 10.

When applying this concept to functions rather than the more general expression, there is the concept of Pure Functions. A function is considered pure if it returns the exact same value given the same parameter values and does not alter any values outside of the function. In other words, a function is pure if it has no dependencies on variables that aren't passed in as parameters or defined in the function and cannot alter any value that is defined outside of the function, even the parameters. The idea of changing no variables outside of the function is known as a function having no side-effects. For an example, look at the following Pure function and Impure function (Written with Java syntax):


int pure(int x, int y) {
    int val = x + y;
    val = val * 2;
    return val;
}
This function is considered pure as the only values that it alters are defined within the function itself, and the returned value only depends on the value of the parameters x and y, no other outside variable alters the returned value.

int globalVal = 3;
int impure(int x, int y) {
     int val = x + y;
     val = val * globalVal;
     globalVal += 1;
     return val;
}
This function is considered impure for two reasons. The first reason is that it is dependent on the external variable globalVal. Due to this, the function will return a different value, even when given the same parameters, if globalVal is ever changed. Secondly, the function modifies the value of the external variable globalVal. This is a side-effect.
Pure functions must return the same value given the same parameters and have no side-effects. There can be no dependence on or changing of external variables.

As stated previously, a Referentially Transparent expression must be logically substitutable with the value that would be returned. In this way, Pure Functions, which return the same value given the same parameter and have no side-effects anywhere else in the code, are required for Referential Transparency. Essentially, an expression is Referentially Transparent if it itself is Pure and every function it calls is Pure as well. This is the defining feature of a Declarative language, the fact that it does not allow for functions to mutate any external values and is consistent in its expressions' returned value. The idea of having all pure functions and not mutating any values of the class itself is known as being stateless and is an important aspect of many languages that build on the Declarative ideology.

As writing referentially transparent expressions are the main requirement of Declarative programming, it is actually possible to write Declaratively even if there are no libraries, frameworks, or language features available for the task you need to complete: you can just write your own referentially transparent functions with, ideally, self-commenting identifiers (self-commenting means that it is essentially clear what is done by the function just by the name itself, take "Where()" for instance).

Also, as just some extra jargon for you, the reason why Declarative programming is considered good is because of the way that there is often code written for common actions in coding. These functions and languages are known as Domain-Specific. Take, for example, the field of creating user interfaces. Oftentimes there will need to be a way to create a button that updates something on being pressed. This is needed so often that entire frameworks and libraries are made just for that domain, for example: Angular, React.js, or Solid.js. Each of these are Domain-Specific, Declarative additions to JavaScript that allow for a much easier, more consistent way to create user interfaces (once learned, of course). They each abstract the creation of user interfaces so a button doesn't need to be implemented by the programmer that uses it.