Functional Programming in Scala by Paul Chiusano and Runar Bjarnason is one of the cornerstones for every functional developer. It demystifies Functional Programming. Step by step you learn what functional programming is and how to apply it in your day-to-day work.
The definition of Functional Programming given by the book is eloquent:
Functional programming (FP) is a style of software development emphasizing functions that don't depend on program state. Functional code is easier to test and reuse, simpler to parallelize, and less prone to bugs than other code.
Paul Chiusano and Runar Bjarnason
In this article, I am going to review the 5 key lessons I learned from the book. I believe these are core principles that will make you a better developer regardless of your programming language of choice.
1. Local reasoning in Functional Programming
You may have heard many claims about why you should use Functional Programming. Pure functions, immutability, composability. Yes, these are the principles of Functional Programming. They guide your code and make it more solid, scalable, and, well, functional.
Nonetheless, the main reason you should consider Functional Programming is Local Reasoning.
When you code using Functional Programming principles, you are making your code more complicated but less complex:
- A Complex System has a lot of interconnected blocks. These blocks all depend on each other, they cannot be studied separately, and their behavior is hard to predict. This is what happens when you are working on a huge non-functional application. You are afraid to make any modification because you risk breaking other code somewhere else.
- A Complicated System is composed of many independent and less simple blocks compared to a complex system. Nonetheless, these blocks can be studied individually and they compose to make the totality of the application. Every block can be seen as a black box, where the type signature tells us the boundaries of each function.
Functional Programming is indeed more complicated. Nonetheless, it allows you to focus on each function in isolation. It unlocks Local Reasoning.
Once you are sure a function works, you can simply use it as a black box. You do not need to understand the complete application to reason about one function. Every function is independent, and the application is simply a composition of small building blocks.
Any hidden or out-of-band assumptions or behavior that prevent us from treating our components (be they functions or anything else) as black boxes make composition difficult or impossible.
I suggest you watch this presentation that explains the idea in more detail:
https://youtu.be/y4HrXkZYouk?t=710
2. Functional programming in practice involves a lot of fitting building blocks together in the only way that makes sense
I like to think about Functional Programming as a puzzle. You have some pieces, which are the parameters of your function. From these pieces, you must compose the final solution dictated by the return type of the function.
This quote from the book explains this idea in simple and practical terms:
When you begin designing a functional library, you usually have some ideas about what you generally want to be able to do, and the difficulty in the design process is in refining these ideas and finding a data type that enables the functionality you want.
There is no magic being Functional Programming. Only more reasoning, logic, and abstraction.
This principle also inspired another design technique called Type-Driven Design. The idea is to allow the types and signatures of the functions to guide your implementation. In fact, in some cases, you will find that there is only one possible implementation given the types of the parameters:
Type-driven development is an approach to coding that embraces types as the foundation of your code - essentially as built-in documentation your compiler can use to check data relationships and other assumptions. With this approach, you can define specifications early in development and write code that's easy to maintain, test, and extend.
https://www.manning.com/books/type-driven-development-with-idris
The authors of Functional Programming in Scala describe this approach while explaining how to design a new API:
The signatures specify what information is given to the implementation, and the implementation is free to use this information however it wants as long as it respects the specified laws. [...] We are instead going to design our 'ideal' API as illuminated by our examples and work backwards from there to an implementation. [...] We write down the type signature for an operation we want, then "follow the types" to an implementation.
3. Use exceptions only if no reasonable program would ever catch the exception
After reading the book, I changed my way of looking at error handling and exceptions. What is an exception after all? The following is the definition of the Exception
class from the dart API:
An
Exception
is intended to convey information to the user about a failure, so that the error can be addressed programmatically. It is intended to be caught, and it should contain useful data fields.https://api.dart.dev/stable/2.13.4/dart-core/Exception-class.html
Functional Programming created classes like Either
and Option
to handle errors more grecefully (and programmatically!). These classes allow you to catch and handle the error at compile-time, so that no insidious bug can creep in in your application later on.
Read more about them in my previous article about Functional Programming in dart with fpdart.
4. Laziness lets us separate the description of an expression from the evaluation of that expression
A lazy function is all about control. Instead of calling the function which immediately returns a result, you add a layer of laziness to postpone the evaluation.
Laziness is at the core of many Functional Programming types, like Task
, IO
, TaskEither
, and more.
The concept of laziness is related to strict and non-strict evaluation:
Non-strictness is a property of a function. To say a function is non-strict just means that the function may choose not to evaluate one or more of its arguments. In contrast, a function always evaluates its arguments.
A simple example is the IO
type. IO
is just a function that is not evaluated until you call run()
. This is own IO
is defined in fpdart:
class IO<A> extends HKT<_IOHKT, A> with Monad<_IOHKT, A> {
final A Function() _run;
const IO(this._run);
/// Execute the IO function.
A run() => _run();
You can make any modification to the function, but unless you call the run()
method, the function will not be evaluated. This means that you have full control over the result of the function!
The principle of laziness allows Functional Programming to move side effects on the boundaries of the application:
Inside every function with side effects is a pure function waiting to get out. [...] Each time we apply it, we make more functions pure and push side effects to the outer layers of the program. We sometimes call these impure functions the 'imperative shell' around the pure core of the program. Eventually, we reach functions that seem necessitate side effects.
5. API as an algebra, or an abstract set of operations along with a set of or properties we assume true
The scary part of Functional Programming is its relationship with math and algebra. The book does an amazing job of demystifying this point. The principle behind is explained by this simple quote:
Although the function you've written may have been motivated by some very specific use case, the signature and implementation may have a more general meaning.
This is the definition of abstraction. You extract a more general representation of the function which you can reuse in many different situations. No more code duplication. Testing becomes easier. Functions become smaller. The code becomes easier to reason and compose.
This is a design principle called Algebraic Design:
We will call it algebraic design: designing our interface first, along with associated laws, and letting this guide our choice of data type representations.
Functional Programming is about expressing a set of primitives behind your code. The build blocks. Designing is about exploring your domain to find these building blocks.
We are interested in understanding what operations are and in finding a primitive derived small yet expressive set of primitives. [...] We will consider algebras in the abstract, by writing code that doesn't just operate on one data type or another but on data types that share a common all algebra.
This is where we can get deeper into the math side of Functional Programming. Functional Programming is related to a branch of mathematics called Category Theory. But remember, you do not need to know advanced math to understand Functional Programming!
There is a powerful idea here, namely, that a type is given meaning based on its relationship to other types (which are specified by the set of functions and their laws), rather than its internal representation. This is a viewpoint often associated with category theory.
If you want to know more, I suggest you watch this presentation for an introduction to Category Theory in Functional Programming:
https://www.youtube.com/watch?v=MvQxNm5gn8g
Functional Programming is a huge topic, and a single book cannot convey everything that you need to know about it. I would like to close this article with a quote from the book:
An abstract topic like this cannot be fully understood all at once. It requires an iterative approach where you keep revisiting the topic from new perspectives. When you discover new monads, new applications of them, or see them appear in a new context, you will inevitably gain new insight. And each time it happens you might think to yourself: "OK, I thought I understood monads before, but now I really get it."
If you would like to learn more about these topics, follow me on Twitter at @SandroMaglione and subscribe to my newsletter here below. Thanks for reading.