Chapters

Hide chapters

Functional Programming in Kotlin by Tutorials

First Edition · Android 12 · Kotlin 1.6 · IntelliJ IDEA 2022

Section I: Functional Programming Fundamentals

Section 1: 8 chapters
Show chapters Hide chapters

Appendix

Section 4: 13 chapters
Show chapters Hide chapters

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 or flatMap on a List<T>. You’re also using Result<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 Strings, 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 Strings you can convert to Ints 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:

  1. Define imperativeSum as a function accepting a List<String> as input and returning an Int.
  2. Initialize the initial value to 0 for the sum to return as the result.
  3. Use an enhanced for to iterate over all the values in List<String>. item contains the current value at every iteration.
  4. Try to convert the String to Int using toInt, adding it to the sum if you can.
  5. If it isn’t possible to convert the String, you catch a NumberFormatException without doing anything. This is very bad practice, and it’s even considered an anti-pattern called “Head in the sand”.
  6. 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:

  1. Take a list of Strings.
  2. Filter out the ones that don’t contain numbers.
  3. Convert the valid Strings to their corresponding Ints.
  4. 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:

  1. Define declarativeSum as a function accepting a List<String> as input and returning an Int, exactly as imperativeSum did.
  2. Use filter to remove the values that can’t convert to Ints from the List<String>.
  3. Convert the String you know is valid to an Int, getting a List<Int>.
  4. Use the predefined sum of List<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 the Strings you don’t want, convert them to Ints 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 Strings. In imperativeSum, you’d need to add if-elses. In declarativeSum, you just add a new filter, 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 for declarativeSum as well. But what you’d need to test is just isValidNumber, as filter, map and sum have already been tested. You really just need to test that the function isValidNumber 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 a List<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:

  1. Invoke times as an extension function for the Int type. In this case, you invoked it on 3.
  2. 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:

  1. Define times as an extension function for Int. You also define a single parameter fn of type () -> Unit, which is the type of any function without input parameters and returning Unit.
  2. Use a for loop to count the number of times related to the receiver.
  3. 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:

  1. double returns the double of the Int value in input.
  2. square returns the square of the Int value in input.

Both the functions map Ints to Ints, 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:

  1. Define a global variable count.
  2. Create impure as a function of type (Int) -> Int.
  3. Increment count.
  4. Use count and the input parameter value 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:

  1. Define addOneAndLog of type (Int) -> Int, which just returns the value it gets in input and adds 1.
  2. Calculate the incremented value and store it in result.
  3. Use println to write a log message on the standard output.
  4. 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:

  1. Making the return type Pair<Int, String> instead of Int.
  2. Returning a Pair of the result 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:

  1. Define strToIntOrNull, which now has the optional type Int? as its return type.
  2. Try to convert the String to Int, but do it in a try block.
  3. Return null in the case of NumberFormatException.

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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.