Book Image

Mastering Functional Programming

Book Image

Mastering Functional Programming

Overview of this book

Functional programming is a paradigm specifically designed to deal with the complexity of software development in large projects. It helps developers to keep track of the interdependencies in the code base and changes in its state in runtime. Mastering Functional Programming provides detailed coverage of how to apply the right abstractions to reduce code complexity, so that it is easy to read and understand. Complete with explanations of essential concepts, practical examples, and self-assessment questions, the book begins by covering the basics such as what lambdas are and how to write declarative code with the help of functions. It then moves on to concepts such as pure functions and type classes, the problems they aim to solve, and how to use them in real-world scenarios. You’ll also explore some of the more advanced patterns in the world of functional programming such as monad transformers and Tagless Final. In the concluding chapters, you’ll be introduced to the actor model, which you can implement in modern functional languages, and delve into parallel programming. By the end of the book, you will be able to apply the concepts of functional programming and object-oriented programming (OOP)in order to build robust applications.
Table of Contents (17 chapters)

Principles of declarative programming

Why declarative programming? How did it appear? To understand declarative programming, we need to first understand how it is different from imperative programming. For a long time, imperative programming has been a de facto industry standard. What motivated people to start switching to the functional style from the imperative style?

In imperative programming, you rely on a set of primitives that your language provides. You combine them in a certain way so as to achieve a functionality that you need. We can understand different things under primitives. For example, these can be loop control structures, or, in the case of collections, operations specific to collections, such as creating a collection and adding or removing elements from a collection.

In declarative programming, you also rely on primitives. You use them to express your program. Yet, in declarative programming, these primitives are much closer to your domain. They can be so close to your domain that the language itself can be regarded as a domain-specific language (DSL). With declarative programming, you are able to create primitives as you go.

In imperative programming, you usually don't create new primitives, but rely on the ones the language provides you with. Let's go through some examples to understand the importance of declarative programming.

Example – go-to versus loops

How imperative turns into declarative is best understood by means of an example. Most likely, you already know the go-to statement. You have heard that using the go-to statement is bad practice. Why? Consider an example of a loop. It is possible to express a loop using only the go-to statement:

#include <iostream>
using namespace std;
int main() {
int x = 0;
loop_start:
x++;
cout << x << "\n";
if (x < 10) goto loop_start;
return 0;
}

From the preceding example, imagine you need to express a while loop. You have the variable x and you need to increment it in a loop by one until it reaches 10. In modern languages such as Java, you would be able to do this using the while loop, but it is also possible to do that using the go-to statement. For example, it is possible to have a label on the increment statement. A conditional statement after it will check whether the variable reached the necessary value. If it did not, we perform a go-to on the line of code to increment a variable.

Why is go-to a bad style in this case? A loop is a pattern. A pattern is an arrangement of two or more logical elements in your code that repeats in different places of your program. In our case, the pattern is the loop. Why is it a pattern? First, it consists of three parts:

  1. The first part is the label that is the entry point to the body of the loop—the point where you jump from the end of the loop to reiterate the loop.
  2. The second part is the condition that must be true in order for the loop to reiterate.
  3. The third part is the statement to reiterate the fact that it is a loop. It is the end of the body of the loop.

Besides being composed of three parts, it also describes an action that is ubiquitous in programming. The action is repeating a chunk of code more than once. The fact that loops are ubiquitous in programming needs no explanation.

If you re-implement the loop pattern each time you need it, things can go wrong. As the pattern has more than one part to it, it can be corrupted by misusing one of the parts, or you could make a mistake when arranging the parts into a whole. It is possible to forget to name the label to which to jump, or to name it incorrectly. You may also forget to define the predicate statement that guards the jump to the beginning of the loop. Or, you could misspell the label to which to jump in the q statement itself. For example, in the following code, we forgot to specify the predicate guard:

int main() {
int x = 0;
loop_start:
x++;
cout << x << "\n";
goto loop_start;
return 0;
}

Example – nested loop

It's pretty hard to get such a simple example wrong, but consider a nested loop. For example, you have a matrix, and you want to output it to the console. This can be done with a nested loop. You have a loop to iterate on with every entry of the 2D array. Another loop nested in that loop examines the row the outer loop is currently working on. It iterates on every element of that row and prints it to the console.

It is also possible to express these in terms of the go-to statement. So, you will have the first label to signify the entry point into the large loop, another label to signify the entry point to the small loop, and you will call the go-to statement at the end of each loop to jump to the beginning of the respective loop.

Let's see how to do that. First, let's define a 2D array as follows:

 int rows = 3;
int cols = 3;
int matrix[rows][cols] = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};

Now, we can loop over it as follows:

 int r = 0;
row_loop:
if (r < rows) {
int c = 0;
col_loop:
if (c < cols) {
cout << matrix[r][c] << " ";
c++;
goto col_loop;
}
cout << "\n";
r++;
goto row_loop;
} return 0;}

You can already see an increase in complexity here. For example, you can perform a go-to from the end of the inner loop to the beginning of the outer loop. This way, only the first item of each column receives output. The program becomes an infinite loop:

 int r = 0;
row_loop:
if (r < rows) {
int c = 0;
col_loop:
if (c < cols) {
cout << matrix[r][c] << " ";
c++;
goto row_loop;
}
cout << "\n";
r++;
goto row_loop;
}

Don't Repeat Yourself (DRY)

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.