1.
Why Functional Programming
Written by Massimo Carli
When you approach a topic like functional programming, the most common questions are:
- Why do you need it?
- What are the benefits of using it?
- Is knowledge of all the theory supporting it necessary?
- Isn’t object-oriented programming enough for writing good quality code?
In this chapter, you’ll answer these and other questions about functional programming. But first, consider these three main points:
- This might surprise you, but you don’t need to know functional programming to write your code. Many professional engineers have been writing code for years without using any functional programming techniques, which is totally fine. But in this chapter, you’ll learn that when you use functional programming, your code will be more readable, robust, reusable and testable.
- Functional programming isn’t all or nothing. Gang of Four design patterns you use with the object-oriented approach are still completely valid.
- Believe it or not, you’re probably already using some functional programming tools. If you write your code in Kotlin, you’ve probably already invoked
map
orflatMap
on aList<T>
. You’re also usingResult<T>
to handle errors. Some functional programming concepts are already there, and this is the approach most standard libraries follow.
But what exactly does “better code” mean? The answer is complex, but in this first chapter, you’ll get a taste of some of the principles that are the pillars of functional programming:
- Declarative approach
- Higher-order functions
- Composition
- Pure functions and testability
- Exception handling
You’ll also have the chance to solve some fun and interesting exercises.
Using a declarative approach
Readability is one of the most important properties that all your code should have. The ability to return to your code after some time and still understand it is vital. If your code is readable, that means other engineers should be able to understand it. And that means they won’t bother you with questions you might not even remember how to answer.
Moving from an imperative approach to a declarative one can drastically improve the readability of your code. This is a book about real-world programming, so an example can be helpful:
Suppose you have a list, List<String>
. Some list elements are numbers, like "123"
. Others are normal String
s, like "abc"
. You want to implement a function, stringSum
, which returns the sum of all the values you can convert from String
to Int
.
For instance, given the following input:
val input = listOf(
"123", "abc", "1ds", "987", "abdf", "1d3", "de1", "88", "101"
)
In this case, the String
s you can convert to Int
s are:
"123", "987", "88", "101"
And the sum would then be:
123 + 987 + 88 + 101 = 1299
You can create a first solution to the problem and write it in the Declarative.kt file in this chapter’s material:
fun imperativeSum(list: List<String>): Int { // 1
var sum = 0 // 2
for (item in input) { // 3
try {
sum += item.toInt() // 4
} catch (nfe: NumberFormatException) { // 5
// Skip
}
}
return sum // 6
}
In this code, you:
- Define
imperativeSum
as a function accepting aList<String>
as input and returning anInt
. - Initialize the initial value to
0
for thesum
to return as the result. - Use an enhanced
for
to iterate over all the values inList<String>
.item
contains the current value at every iteration. - Try to convert the
String
toInt
usingtoInt
, adding it to thesum
if you can. - If it isn’t possible to convert the
String
, you catch aNumberFormatException
without doing anything. This is very bad practice, and it’s even considered an anti-pattern called “Head in the sand”. - Return the
sum
.
To test this solution, just add and run the following code using the list declared above:
fun main() {
println("Sum ${imperativeSum(input)}")
}
As expected, you get:
Sum 1299
This is an imperative approach because you’re telling the program exactly what to do and you’re doing this in its own language. Now, imagine you want to solve the same problem, but explain it to a friend of yours in plain English. You’d just say:
- Take a list of
String
s. - Filter out the ones that don’t contain numbers.
- Convert the valid
String
s to their correspondingInt
s. - Calculate their sum.
This logic is closer to how you think, and it’s much easier to explain and remember. But how can you translate it into code?
Add this in the same Declarative.kt file:
fun declarativeSum(list: List<String>): Int = list // 1
.filter(::isValidNumber) // 2
.map(String::toInt) // 3
.sum() // 4
In this code, you:
- Define
declarativeSum
as a function accepting aList<String>
as input and returning anInt
, exactly asimperativeSum
did. - Use
filter
to remove the values that can’t convert toInt
s from theList<String>
. - Convert the
String
you know is valid to anInt
, getting aList<Int>
. - Use the predefined
sum
ofList<Int>
.
This code won’t compile because you still need to define isValidNumber
, which is a function you can implement like this:
fun isValidNumber(str: String): Boolean =
try {
str.toInt()
true
} catch (nfe: NumberFormatException) {
false
}
Here, you just try to convert the String
to Int
and return true
if successful and false
otherwise.
Now, add and run this code:
fun main() {
// ...
println("Sum ${declarativeSum(input)}")
}
Which gives you the same result:
Sum 1299
You might argue that in the declarative solution, you still use that ugly way of testing whether String
can be converted to Int
. If String
can be converted, you also invoke toInt
twice. You’ll come back to this later, but at the moment, what’s important to note is that:
-
declarativeSum
is written in a way that’s closer to how you think and not to how the compiler thinks. If you read the code, it does exactly what you’d describe in plain English. Filter out theString
s you don’t want, convert them toInt
s and calculate the sum. The code is noticeably more readable. - Good code is also easier to change. Imagine you have to change the way you filter
String
s. InimperativeSum
, you’d need to addif-else
s. IndeclarativeSum
, you just add a newfilter
, passing a function with the new criteria. - Testability is a must in the modern software industry. How would you test
imperativeSum
? You’d create different unit tests, checking that the function’s output for different input values is what you expect. This is true fordeclarativeSum
as well. But what you’d need to test is justisValidNumber
, asfilter
,map
andsum
have already been tested. You really just need to test that the functionisValidNumber
does what you expect.
Functional programming means programming with functions, and the declarative approach allows you to do it very easily. In declarativeSum
, this is obvious because of the use of isValidNumber
and String::Int
, which you pass as parameters of functions like map
and filter
. These are examples of a particular type of function you call a higher-order function.
Exercise 1.1: Implement the function
sumInRange
, which sums the values in aList<String>
within a given interval. The signature is:fun sumInRange(input: List<String>, range: IntRange): Int
Give it a try, and check your answer with the solution in Appendix A.
Higher-order functions
In this chapter’s introduction, you learned that the functions map
and flatMap
— which you’re probably already using — are implementations of some important functional programming concepts. In Chapter 11, “Functors”, you’ll learn about map
, and in Chapter 13, “Understanding Monads”, you’ll learn about one of the most interesting concepts: monads. Monads provide implementations for the flatMap
function.
At this stage, it’s important to note how map
and flatMap
are examples of a specific type of function: They both accept other functions as input parameters. Functions accepting other functions as input parameters — or returning functions as return values — are called higher-order functions. This is one of the most significant concepts in functional programming. In Chapter 5, “Higher-Order Functions”, you’ll learn all about them and their relationship with the declarative approach.
As a very simple example, create a function called times
that runs a given function a specific number of times. Open HigherOrder.kt and add the following code:
fun main() {
3.times { // 1
println("Hello") // 2
}
}
This code doesn’t compile yet, but here, you:
- Invoke
times
as an extension function for theInt
type. In this case, you invoked it on3
. - Pass a lambda containing a simple
println
with the “Hello” message.
Running this code, you’d expect the following output:
Hello
Hello
Hello
The code prints the “Hello” message three times. Of course, to make the times
function useful, you should make it work for all code you want to repeat. This is basically a function accepting a lambda that, in this case, is a function of type () -> Unit
.
Note: The previous sentence contains some important concepts, like function type and lambda, you might not be familiar with yet. Don’t worry — you’ll come to understand these as you work your way through this book.
times
is a simple example of a higher-order function because it accepts another function as input. A possible implementation is the following, which you should add to HigherOrder.kt:
fun Int.times(fn: () -> Unit) { // 1
for (i in 1..this) { // 2
fn() // 3
}
}
In this code, you:
- Define
times
as an extension function forInt
. You also define a single parameterfn
of type() -> Unit
, which is the type of any function without input parameters and returningUnit
. - Use a
for
loop to count the number of times related to the receiver. - Invoke the function
fn
you pass in input.
Now, you can run main
, resulting in exactly what you expect as output: “Hello” printed three times.
Like it or not, this implementation works, but it’s not very, ehm, functional. The IntRange
type provides the forEach
function, which is also a higher-order function accepting a function of a slightly different type as input. Just replace the previous code with the following:
fun Int.times(fn: () -> Unit) =
(1..this).forEach { fn() }
forEach
iterates over an Iterable<T>
, invoking the function you pass as a parameter using the current value in input. In the previous case, you don’t use that parameter, but you might’ve written:
fun Int.times(fn: () -> Unit) =
(1..this).forEach { _ -> fn() } // HERE
As mentioned, you’ll learn everything you need to know about this in Chapter 5, “Higher-Order Functions”.
Exercise 1.2: Implement
chrono
, which accepts a function of type() -> Unit
as input and returns the time spent to run it. The signature is:fun chrono(fn: () -> Unit): Long
Give it a try, and check your answer with the solution in Appendix A.
Composition
As mentioned, functional programming means programming using functions in the same way that object-oriented programming means programming with objects. A question without an obvious answer could be: Why do you actually need functions? In Chapter 2, “Function Fundamentals”, and Chapter 3, “Functional Programming Concepts”, you’ll have a very rigorous explanation using category theory. For the moment, think of functions as the unit of logic you can compose to create a program. Decomposing a problem into smaller subproblems to better understand them is something humans do every day. Once you’ve decomposed your problem in functions, you need to put them all together and compose them in the system you’ve designed.
Note: This also happens with objects. You use the classes as a way to model the different components that collaborate to achieve a specific task.
The most important part of functional programming involves composition.
Open Composition.kt in this chapter’s material and add the following code:
fun double(x: Int): Int = 2 * x // 1
fun square(x: Int): Int = x * x // 2
These are two very simple functions:
-
double
returns the double of theInt
value in input. -
square
returns the square of theInt
value in input.
Both the functions map Int
s to Int
s, and you can represent them as functions of type (Int) -> Int
.
Composing double
with square
means invoking the first function and then passing the result as input for the second. In code, this is:
fun main() {
val result = double(square(10)) // HERE
println(result)
}
Here, you invoke square
by passing 10
in input. Then, you use the result to invoke double
. In that case, the output will be:
200
The question at this point is different. Because square
and double
are both functions of type (Int) -> Int
, you can assume that a third function exists: squareAndDouble
. It’s of the same type, (Int) -> Int
, and does the same as invoking square
first and then double
. Here, the input value doesn’t matter anymore. You’re thinking in terms of functions. A simple — and obvious — way to implement that function is the following:
fun squareAndDouble(x: Int) = double(square(x))
This isn’t very interesting, though. In the previous section, you learned what a higher-order function is. So now, the question is: Can you implement a higher-order function that, given two functions as input, returns a third function that’s the composition of the two? Yes, you can! Of course, the two functions need to be compatible, meaning the output type of the first needs to be compatible with the input type of the second. Besides that, you want a new function that creates the composition of the other two functions. In Chapter 8, “Composition”, you’ll learn all about this. At the moment — spoiler alert — you can define compose
like the following, which you should add in Composition.kt:
infix fun <A, B, C> ((A) -> B).compose(
g: (B) -> C
): (A) -> C = { a ->
g(this(a))
}
Note: Don’t worry if you don’t understand the previous definition. Teaching how to write functions like this is the goal of the following chapters! :]
Now, update main
like this:
fun main() {
// ...
val squareAndDouble = ::square compose ::double // HERE
println(squareAndDouble(10))
}
squareAndDouble
is a function that’s the composition of square
and double
. You can simply invoke it like any other function. Also note that compose
works for every pair of functions that are composable, which means the output of the first must be a type that’s compatible with the input of the second.
That’s nice, but do you really need a compose
function? Why not just invoke functions the same way you did with square
and double
? The answer is that functional programming is magic. In this book, you’ll learn the main concepts of category theory. You’ll also prove that using a functional programming approach will make your code more robust, reusable and even more efficient.
Unfortunately, not all functions are like double
and square
. Other functions aren’t so easy to compose and are impure. Functional programming is about pure functions. But what are pure functions, and why are they so important?
Pure functions and testability
In Chapter 3, “Functional Programming Concepts”, you’ll learn all about pure functions.
Just to give you an idea, they’re functions whose work only depends on the input parameter, and the function doesn’t change the world outside the function itself when invoked. double
and square
are pure functions. They return a value that depends only on the input value and nothing else. The following, which you can add in Pure.kt, isn’t pure:
var count = 0 // 1
fun impure(value: Int): Int { // 2
count++ // 3
return value + count // 4
}
This is impure for different reasons. Here, you:
- Define a global variable
count
. - Create
impure
as a function of type(Int) -> Int
. - Increment
count
. - Use
count
and the input parametervalue
to calculate the value to return.
impure
isn’t pure because the output doesn’t depend only on the input parameter. Invoking impure
multiple times with the same input parameter will return different values.
Another example of an impure function is the following, which you can add to the same file:
fun addOneAndLog(x: Int): Int { // 1
val result = x + 1 // 2
println("New Value is $result") // 3
return result // 4
}
In this case, you:
- Define
addOneAndLog
of type(Int) -> Int
, which just returns the value it gets in input and adds1
. - Calculate the incremented value and store it in
result
. - Use
println
to write a log message on the standard output. - Return the
result
.
If you invoke addOneAndLog
multiple times with the same value in input, you’ll always get the same value in output. Unfortunately, addOneAndLog
isn’t pure because the println
changes the state of the world outside the function itself. This is a typical example of a side effect. Impure functions are difficult to test because you need to somehow replicate — using mocks or fakes — the external world, which impure functions change. In the case of addOneAndLog
, you’d need to abstract the standard output, introducing complexity.
Now, you have bad news and good news. The bad news is that all the great principles of functional programming you’ll learn in this book are only valid for pure functions. The good news is that you’ll also learn how to make impure functions pure.
How can you make addOneAndLog
pure, then? A classic way is to move the effect to become part of the result type. Replace the existing addOneAndLog
implementation with the following:
fun addOneAndLog(x: Int): Pair<Int, String> { // 1
val result = x + 1
return result to "New Value is $result" // 2
}
The changes you made from the previous implementation are:
- Making the return type
Pair<Int, String>
instead ofInt
. - Returning a
Pair
of theresult
and the message you were previously printing.
Now, addOneAndLog
is a function of type (Int) -> Pair<Int, String>
. More importantly, it’s now pure because the output only depends on the value in input, and it doesn’t produce any side effects. Yes, the responsibility of printing the log will be that of some other component, but now addOneAndLog
is pure, and you’ll be able to apply all the beautiful concepts you’ll learn in this book.
But, there’s a “but”…
The first implementation of addOneAndLog
had type (Int) -> Int
. Now, the type is (Int) -> Pair<Int, String>
. What happens if you need to compose addOneAndLog
with itself or another function accepting an Int
in input? Adding the effect as part of the result type fixed purity but broke composition. But functional programming is all about composition, and it must have a solution for this as well. Yes! The solution exists and is called a monad! In Chapter 13, “Understanding Monads”, you’ll learn everything you need to know about monads, solving not just the problem of addOneAndLog
, but all the problems related to the composition of functions like that.
Exception handling
Exceptions are a typical example of side effects. As an example you should be familiar with, open ExceptionHandling.kt and add the following code:
fun strToInt(str: String): Int = str.toInt()
As you know, this function throws a NumberFormatException
if the String
you pass as a parameter doesn’t contain an Int
.
Even if it’s not visible in the function’s signature, the exception is a side effect because it changes the world outside the function. Then, strToInt
isn’t pure. In the previous section, you already learned one way to make this function pure: Move the effect as part of the return type. Here, you have different options. The simplest is the one you get with the following code:
fun strToIntOrNull(str: String): Int? = // 1
try {
str.toInt() // 2
} catch (nfe: NumberFormatException) {
null // 3
}
In this code, you:
- Define
strToIntOrNull
, which now has the optional typeInt?
as its return type. - Try to convert the
String
toInt
, but do it in atry
block. - Return
null
in the case ofNumberFormatException
.
This function now is pure. You might argue that the try/catch
is still there, so it’s the exception. It’s crucial to understand that functional programming doesn’t mean removing side effects completely, but it means being able to control them. strToIntOrNull
now returns the same output for the same input, and it doesn’t change anything outside the context of the function.
In the case of strToInt
, the information you want to bring outside is minimal: You just want to know if you have an Int
value or not. In another case, you might need more information like, for instance, what specific exception has been thrown. A possible, idiomatic Kotlin alternative is the following:
fun strToIntResult(str: String): Result<Int> =
try {
Result.success(str.toInt())
} catch (nfe: NumberFormatException) {
Result.failure(nfe)
}
Here, you encapsulate the Int
value or the error
in a Result<Int>
.
You’ll learn all about error handling in a functional way in Chapter 14, “Error Handling With Functional Programming”. What’s important now is understanding how even the existing Kotlin Result<T>
is there as a consequence of the application of some fundamental functional programming concepts a model engineer should know to create high-quality code.
Key points
- While object-oriented programming means programming with objects, functional programming means programming with functions.
- You decompose a problem into many subproblems, which you model with functions.
- Higher-order functions accept other functions as input or return other functions as return values.
- Category theory is the theory of composition, and you use it to understand how to compose your functions in a working program.
- A pure function’s output value depends only on its input parameters, and it doesn’t have any side effects.
- A side effect is something that a function does to the external world. This can be a log in the standard output or changing the value of a global variable.
- Functional programming works for pure functions, but it also provides the tools to transform impure functions into pure ones.
- You can make an impure function pure by moving the effects to make them part of the return value.
- Functional programming is all about composition.
- Error handling is a typical case of side effects, and Kotlin gives you the tools to handle them in a functional way.
Where to go from here?
Great! In this chapter, you had the chance to taste what functional programming means. In this book, you’ll learn much more using both a pragmatic method and a theoretical and rigorous one. You’ll also have the chance to see where these concepts are already used. You’ll be able to recognize them and apply all the magic only math can achieve. You’ve got a lot of learning ahead of you!