What is Imperative Programming?
The imperative programming paradigm uses a sequence of statements to modify a program's state through the use of variables. The goal of the imperative paradigm is to specify how a program should execute through explicit instructions.
It's common to see imperative programming compared to declarative programming, which is the philosophy behind languages where the programmer specifies what to do, rather than how to do it.
> If this is confusing, for now consider this: a cookbook instructs step-by-step how to cook a recipe, a restaurant menu shows what you can order, and you don't need to worry how it's made.
The imperative paradigm serves as the basis for later paradigms like the structured, procedural, and object-oriented paradigms.
An Introduction
If you're new to programming, I salute your paradigms-first approach to learning. Seriously, send me an email and let me know how it goes, I've always been curious if it's effective...
If you've been programming for a while and your first programming language was Python, JavaScript, C++, or even C, it's very likely that the first few programs you wrote very closely resembled a primitive imperative programming approach. That is, you weren't quite acquainted with the idea of functions, recursion, or structures yet. If I had to guess, you might've started off writing programs in a similar spirit to this:
int myAge = 23;
boolean canDrive;
if(myAge > 16) {
canDrive = true;
}
else {
canDrive = false;
}
print("Am I old enough to drive: " + canDrive);
int ageGuess = 1;
while(ageGuess <= myAge) {
print("Are you " + ageGuess + "?");
ageGuess++;
}
print("Stop!");
However, if your first experience with a language was with SQL, HTML*, or CSS*, for example, then you might've been first experienced with what is known as the Declarative Paradigm. We'll talk more about this later!
> * Before you blast an angry email questioning my legitimacy as a programmer, HTML and CSS aren't programming languages, but they do feature many declarative properties, which makes them worth exemplifying!
Characteristics of the Imperative Paradigm
The definition of the imperative paradigm describes using statements and variables as the tools on our utility belt to write useful programs. Let's go over the characteristics of the imperative paradigm:
Mutable Variables and Assignment
Variables are the secret sauce of the imperative paradigm. They're what makes the interesting and useful things possible. Collectively, they make up what is known as the state of a program. Some variables have the ability to change their value after they've been set to an initial value. These variables are called mutable variables, which variables with values that cannot be changed once set are called immutable variables. Consider the following program:
var first = 5
var second = 17
var result
result = first + second
print(result)
Though our little program may not be solving the mysteries of the universe, it will help us solve the mystery of variables.
When we want a new variable within our program, we need to declare that variable. In our program, we've declared the variables first
and second
, and assigned their values to be 5
, and 17
by using the assignment operator =
. In common programming speech, we can say we've initialized a variable when we assign its value for the first time.
With that said, take a look at our variable result
; we've declared it on line 3, but we haven't initialized it yet. If we skipped line 4, and just went straight to print(result)
, what would it output on the screen? Many modern programming languages will stop you before you can even run the program, citing "you're trying to use the variable result before initializing its value!" Lucky for us, we do initialize the variable on line 4, saying that it's the sum of the previous two variables.
> Even though we didn't assign a value to result
on the same line we declared it, we can still say we've initialized it on line 4, because it's the first time we're assigning a value to it.
Let's look at another program that displays the same error:
var first = 5
var result
result = result + first // Oh no!
Even though we're to assigning a value to result
, we're also trying to read the value of it before we've actually assigned it!
Now that we've covered the basics of variable declaration and assignment, consider the following:
var first = 5
var second
first + 5 = second // Error!
Clearly this code is wrong, but why? At first glance, it might appear that we're taking the output of first + 5
and assigning it to second
, but this isn't so.
To understand why this code is wrong, we need understand the difference between what are known as l-values and r-values.
L-Values, R-Values, and the Assignment Operator
The terms l-value and r-value get their names because they appear on the left and right side of the assignment operator. In other words, a proper assignment must look like this:
<div style="width:100%;text-align:center;font-style:italic;font-size:150%;">l-value = r-value</div>
This is simple enough, but it doesn't tell us much about what and l-value or an r-value is. To get a better grasp, we need to peel back the layers of our computer and take a look at memory.
If we look at memory in a simplified way, it is essentially a long line of compartments, known as addresses that contain values. Below are four addresses, with the values that currently reside within them:
Address | 0x0000 | 0x0001 | 0x0002 | 0x0003 |
---|---|---|---|---|
Value (binary) | 0110 1001 | 1100 0010 | 1101 1111 | 1001 0010 |
Some types of data need more than 1 byte to express their possible values. For instance, an integer typically needs 4 bytes of memory to express whole numbers ranging from -2,147,483,648 through 2,147,483,648. If we wanted bigger numbers, we would need to use more memory, or a different way to represent the numbers, like floating point notation. Now that we have a basic understanding of the structure of memory, we can start to demystify l-values and r-values.
An l-value is a variable or pointer that refers to a specific, modifiable address in memory which we intend to write an r-value into. L-values appear on the left-hand side of the assignment operator. Some parts of memory are designated as non-modifiable, like when they being used by immutable variables, which is why we need this distinction.
An r-value is an expression that can be evaluated to a plain value in order to be read into the l-value. It is important to know that a variable or pointer can be both r-values and l-values, in other words, whether or not a variable is an l-value or an r-value depends on the context that it is being used in. Let's practice with some examples:
var counter // counter is an l-value
counter = 5 // counter is an l-value, 5 is an r-value.
// ...
counter = counter + 1 // counter is an l-value, AND an r-value!
How is it that counter
is both an l-value and an r-value? This is where context is key! When we're declaring the variable and assigning it a value, it is an l-value because we are assigning the value 5 to the place in memory that the variable owns. However, on line 3 we need to read the value at the address that counter
owns, evalue the expression 5 + 1
, and then finally write the result into the l-value.
Recall our code from earlier that was troublesome:
var first = 5
var second
first + 5 = second
With our new knowledge of l-values and r-values, we can see that there are actually two problems with the code on line 3:
second
is an r-value and has not been initialized, so we can't read that value into the left-hand side.- More importantly,
first + 5
doesn't refer to any modifiable point in memory, so it isn't a valid l-value!
Pay close attention to the phrase "doesn't refer to any modifiable point in memory", because it holds a subtle clue that often trips up many programmers, and it has to do with pointers.
If you're unfamiliar with pointers, or have never used languages like C or C++, this may be something to come back to later. Nonetheless, suppose we're writing a program in C and we have the following code:
int *first = 5; // *first is an l-value, 5 is an r-value
int *second = 10; //...
*(first + 1) = second // This is valid (but weird looking) code!
Wait a minute, how can we have an expression like that on the left-hand side when in the example before we couldn't? This has to do once again with context. The expression *(first + 1)
evaluates to the address 4 bytes* ahead of the variable first
, which we dereference in order to store a value at the address. When we evaluate this expression, we end up with a reference to a point in memory! Because it is referring to a place in memory, it is a valid l-value!
> *The reason it moves 4 bytes instead of 1 byte is because the size of an int*
is 4 bytes, and pointer arithmetic multiplies the number by the size of the datatype.
Statements
In the imperative paradigm, we can define a program as being a sequence of statements that allow us to achieve a useful result. We've already had experience with statements in the form of the assignment operator =
.
An important distinction to make is the difference between a statement and an expression. Generally speaking a statement is composed of expressions or other statements, while an expression is a collection of operators that can be evaluated to a single value.
There are many types of statements in programming, many of them acting as control structures to help dictate the flow of logic and what statements to execute based on specific values and variables. Control structures include if-else statements as well as loops to give two examples. However, to properly cover these control structures, we'll have to dive into the structured programming paradigm, the next step of evolution for imperative programming languages.