One of the fundamental rules of engineering is to create abstractions for logic that repeats. The pattern of the loop is ubiquitous. You can experience it in almost any program. Hence, it is reasonable to abstract away. This is why contemporary languages, such as Java or C++, have their own built-in mechanisms for loops.
The difference it makes is that, now, the entire pattern consists of one component only, that is, the keyword that must be used with a certain syntax:
#include <iostream>
using namespace std;
int main() {
int rows = 3;
int cols = 3;
int matrix[rows][cols] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) cout << matrix[r][c] << " ";
cout << "\n";
}
}
What happened here is that we gave a name to the pattern. Every time we need this pattern, we do not implement it from scratch. We call the pattern by its name.
This calling by name is the main principle of declarative programming: implement patterns that repeat only once, give names to those patterns, and then refer to them by their name anywhere we need them.
For example, while or for loops are patterns of loops. They are abstracted away and implemented on a language level. The programmer can refer to them by their names whenever they need a loop. Now, the chance of making an error is much less likely because the compiler is aware of the pattern. It will perform a compile-time check on whether you are using the pattern properly. For example, when you use the while statement, the compiler will check whether you have provided a proper condition. It will perform all the jump logic for you.
So, you do not need to worry whether you jumped to the correct label, or that you forgot to jump at all. Therefore, there is no chance of you jumping from the end of the inner loop to the start of the outer loop.
What you have seen here is the transition from an imperative to a declarative style. The concept you need to understand here is that we made the programming language aware of a certain pattern. The compiler was forced to verify the correctness of the pattern at compile time. We specified the pattern once. We gave it a name. We made the programming language enforce certain constraints on the programmer that uses this name. At the same time, the programming language takes care of the implementation of the pattern, meaning that the programmer does not need to be concerned with all the algorithms that were used to implement the pattern.
So, in declarative programming, we specify what needs to be done without specifying how to do it. We notice patterns and give them names. We implement these patterns once and call them by name afterward whenever we need to use them. In fact, modern languages, such as Java, Scala, Python, or Haskell do not have the support of the go-to statement. It seems that the vast majority of the programs expressed with the go-to statement can be translated into a set of patterns, such as loops, that abstract away go-to statements. Programmers are encouraged to use these higher-level patterns by name, rather than implementing the logic by themselves using lower-level go-to primitives. Next, let's see how this idea develops further using the example of declarative collections and how they differ from imperative ones.