What is functional programming?

Most of us started programming using an imperative style of coding. What do we mean by this? It means that we give the computer a set of instructions or commands one after the other. As we do this, we change the system’s state with each step that we take. We’re naturally drawn to this approach because of its initial simplicity. On the other hand, as programs grow in size and become more complicated, this seeming simplicity leads to the opposite; complexity arises and takes the place of what we initially intended to do. The end result is code which isn’t maintainable, difficult to test, hard to reason about and (possibly worst of all) full of bugs. The initial velocity that we delivered features slows down substantially until even a simple enhancement to our program becomes a slow and laborious task.

Functional programming is an alternative style to the imperative which addresses the problems mentioned above. In this article we look at a simple example where a piece of imperative code with side effects (we’ll understand what that means shortly) is transformed into the functional style by a sequence of refactoring steps. The eradication of these side effects is one of the core concepts behind functional programming, and this is the highlights of this article. We understand the dangers these effects pose and see how to extract them from our code, bringing us back to the safe place of simplicity where we departed from when we initially set out on our journey.

At this point, it’s also worth mentioning that this article is about functional programming using Kotlin to demonstrate the principles of this programming paradigm. Moreover, the focus isn’t on Kotlin as a language but rather on deriving the concepts used in functional programming. In fact, many of the constructs that we build aren’t available in Kotlin; they’re only found in third party libraries such as Arrow [1]. This article teaches you functional programming from first principles that could be applied to many programming languages, not only to Kotlin.

Functional programming (FP) is based on a simple premise with far-reaching implications: we construct our programs using only pure functions—functions which have no side effects. What are side effects? A function has a side effect if it does something other than return a result, for example:

  • Modifying a variable
  • Modifying a data structure in place
  • Setting a field on an object
  • Throwing an exception or halting with an error
  • Printing to the console or reading user input
  • Reading from or writing to a file
  • Drawing on the screen

Consider what programming would be like without the ability to do these things, or with significant restrictions on when and how these actions can occur. It may be difficult to imagine. How is it even possible to write useful programs at all? If we can’t reassign variables, how do we write simple programs like loops? What about working with data that changes, or handling errors without throwing exceptions? How can we write programs that must perform IO, like drawing to the screen or reading from a file?

The answer is that functional programming is a restriction on how we write programs, but not on what programs we can express. Following the discipline of FP is tremendously beneficial because of the increase in modularity that we gain from programming with pure functions. Because of their modularity, pure functions are easier to test, reuse, parallelize, generalize, and reason about. Furthermore, pure functions are much less prone to bugs.

The benefits of FP: a simple example

Let’s look at an example that demonstrates some of the benefits of programming with pure functions. The point is to illustrate some basic ideas and this will be your first exposure to Kotlin’s syntax. You won’t become fluent, but as long as you have a basic idea of what the code is doing, this is what’s important.

A program with side effects

Suppose we’re implementing a program to handle purchases at a coffee shop. We’ll begin with a Kotlin program which uses side effects in its implementation (also called an impure program).

Listing 1. A Kotlin program with side effects

class Cafe {

        fun buyCoffee(cc: CreditCard): Coffee {       

            val cup = Coffee()          1

            cc.charge(cup.price)      2

            return cup                     3

        }

    }

 

  1. Instantiate a new cup of coffee.
  2. Charge the credit card. A side effect!
  3. Explicitly return the cup.

The class keyword introduces a class, much like in Java. Its body is contained in curly braces, { and }. A method of the class is declared by the fun keyword. A method parameter named cc of type CreditCard is followed by the Coffee return type, separated by :. The method body consists of a block within curly braces. A new Coffee is subsequently instantiated. Next, a method call is made on the charge method of the credit card. Lastly, the return keyword passes cup back to the caller of the method.

We gain a lot of convenience from using Kotlin. For example, no semicolons are required at the end of each statement, instead newlines are used as statement delimiters. We also don’t need to provide a new keyword when instantiating objects.

The line cc.charge(cup.price) is an example of a side effect. Charging a credit card involves some interaction with the outside world—suppose it requires contacting the credit card provider via some web service, authorizing the transaction, charging the card, and (if successful) persisting a record of the transaction for later reference. In contrast, our function returns a Coffee and these other actions happen on the side, hence the term “side effect.”

As a result of this side effect, the code is difficult to test. We don’t want our tests to contact the credit card provider and charge the card! This lack of testability suggests a design change: arguably, CreditCard shouldn’t have any knowledge baked into it about how to contact the credit card provider to execute a charge, nor should it have knowledge of how to persist in recording this charge in our internal systems. We can make the code more modular and testable by letting CreditCard be agnostic of these concerns and passing a Payments object into buyCoffee.

Listing 2. Adding a payments object

class Cafe {

        fun buyCoffee(cc: CreditCard, p: Payments): Coffee {

            val cup = Coffee()

            p.charge(cc, cup.price)

            return cup

        }

    }

 

Though side effects still occur when we call p.charge(cc, cup.price), we’ve regained some testability. Payments can be an interface, and we can write a mock implementation of this interface which is suitable for testing, but this isn’t ideal either. We’re forced to make Payments an interface, when a concrete class may have been fine otherwise, and any mock implementation will be awkward to use. For example, it might contain some internal state that we’ll have to inspect after the call to buyCoffee, and our test needs to make sure this state has been appropriately modified (mutated) by the call to charge. We can use a mock framework or similar to handle this detail for us, but this all feels like overkill if we want to test that buyCoffee creates a charge equal to the price of a cup of coffee.

Separate from the concern of testing, there’s another problem: it’s difficult to reuse buyCoffee. Suppose a customer, Alice, wants to order twelve cups of coffee. Ideally, we could reuse buyCoffee for this, perhaps calling it twelve times in a loop. As it’s currently implemented, it involves contacting the payment provider twelve times, authorizing twelve separate charges to Alice’s credit card! That adds more processing fees and isn’t good for Alice or the coffee shop.

What can we do about this? We could write a whole new function, buyCoffees, with special logic for batching up the charges. Here, that might not be such a big deal because the logic of buyCoffee is simple, but in other cases the logic we need to duplicate may be non-trivial, and we should mourn the loss of code reuse and composition!

A functional solution: removing the side effects

The functional solution is to eliminate side effects and have buyCoffee return the charge as a value in addition to returning the Coffee. The concerns of processing the charge by sending it off to the credit card provider, persisting a record of it, and so on, is handled elsewhere.

Figure 1. A call to buyCoffee, with and without side effect

Listing 3. A more functional approach to buying coffee

class Cafe {

        fun buyCoffee(cc: CreditCard): Pair<Coffee, Charge> {

            val cup = Coffee()

            return Pair(cup, Charge(cc, cup.price))

        }

    }

 

Here we’ve separated the concern of creating a charge from the processing or interpretation of that charge. The buyCoffee function now returns a Charge as a value along with the Coffee. We’ll see shortly how this lets us reuse it more easily to purchase multiple coffees with a single transaction. But what is Charge? It’s a data type we invented containing a CreditCard and an amount, equipped with a handy function, combine, for combining charges with the same CreditCard:

Listing 4. Charge as a data type

data class Charge(val cc: CreditCard, val amount: Float) {               1

        fun combine(other: Charge): Charge =                                      2

            if (cc == other.cc)                                                                   3

               Charge(cc, amount + other.amount)                                  4

            else throw Exception(“Cannot combine charges to different cards”)

    }

 

  1. A data class with immutable fields.
  2. A combine function combining charges for the same credit card.
  3. Ensure it’s the same card, otherwise throw an exception.
  4. A new Charge is returned, combining the amount of this and the other.

A data class has one primary constructor whose argument list comes after the class name (here, Charge). The parameters in this list become public immutable fields of the class and can be accessed using the usual object-oriented dot notation, as in other.cc.

If a method body has a single expression, it needn’t be surrounded by curly braces but can instead be declared with =.

An if expression has the same syntax as in Java, but it also returns a value equal to the result of the expression. If cc == other.cc, then combine returns Charge(..); otherwise the exception in the else branch is thrown.

The syntax for throwing exceptions is the same as in Java and many other languages.

Now let’s look at buyCoffees, to implement the purchase of n cups of coffee. Unlike before, this can now be implemented in terms of buyCoffee, as we had hoped.

Listing 5. Buying multiple cups with buyCoffees

class Cafe {

        fun buyCoffee(cc: CreditCard): Pair<Coffee, Charge> = TODO()

        fun buyCoffees(cc: CreditCard, n: Int): Pair<List<Coffee>, Charge> {

            val purchases: List<Pair<Coffee, Charge>> = List(n) { buyCoffee(cc) }  1

            val (coffees, charges) = purchases.unzip()                                              2

            return Pair(coffees, charges.reduce { c1, c2 -> c1.combine(c2) })         3

        }

    }

 

  1. Create a self-initialized List.
  2. Split the list of Pairs into two separate lists.
  3. Produce the output pairing coffees to a combined single Charge.

The example takes two parameters: a CreditCard, and the Int number of coffees to be purchased. After the Coffees have been successfully purchased, they’re placed into a single linked List data type. The list is initialized using the List(n) { buyCoffee(cc) } syntax, where n describes the number of coffees, and { buyCoffee(cc) } a function which is used to initialize each element of the list.

An unzip is then used to destructure the list of pairs into two separate lists, each representing one side of the Pair. Destructuring is the process of extracting values from a complex data type. We’re left with the coffees list being a List<Coffee>, and charges being a List<Charge>. The final step involves reconstructing the data into the required output. This is done by constructing a Pair of List<Coffee> mapped to the combined Charges for all the Coffees in the list. reduce is an example of a higher-order function

Overall, this solution is a marked improvement—we’re now able to reuse buyCoffee directly to define the buyCoffees function, and both functions are trivially testable without having to define complicated mock implementations of some Payments interface! In fact, the Cafe is now completely ignorant of how the Charge values are processed. We can still have a Payments class for processing charges, but Cafe doesn’t need to know about it.

Making Charge into a first-class value has other benefits we might not have anticipated; we can more easily assemble business logic for working with these charges. For instance, Alice may bring her laptop to the coffee shop and work there for a few hours, making occasional purchases. It might be nice if the coffee shop could combine these purchases Alice makes into a single charge, again saving on credit card processing fees. Because Charge is first-class, we can now add the following extension method to List<Charge> in order to coalesce any same-card charges:

Listing 6. Coalesce the charges

fun List<Charge>.coalesce(): List<Charge> =

        this.groupBy { it.cc }.values

            .map { it.reduce { a, b -> a.combine(b) } }

 

Let’s focus on the body of the extension method for now. Note that we’re passing functions as values to the groupBy, map, and reduce functions. The statements {it.cc} and {a, b → a.combine(b)} are syntax for anonymous functions. You may find this kind of code difficult to read because the notation is compact. This function takes a list of charges, groups them by the credit card used, and then combines them into a single charge per card. It’s perfectly reusable and testable without any additional mock objects or interfaces. Imagine trying to implement the same logic with our first implementation of buyCoffee!

This is a taste of why functional programming has the benefits claimed, and this example is intentionally simple. If the series of refactoring used here seems natural, obvious, unremarkable, or standard practice, this is good. FP is merely a discipline that takes what many consider a good idea to its logical endpoint, applying the discipline even in situations where its applicability is less obvious. The consequences of consistently following the discipline of FP are profound and the benefits enormous. FP is a truly radical shift in how programs are organized at every level— from the simplest of loops to high-level program architecture. The style that emerges is quite different, but it’s a beautiful and cohesive approach to programming that we hope you come to appreciate.

You can save 40% off Functional Programming in Kotlin, as well as of all other Manning books and videos.

Just enter the code pskotlin40 at checkout when you buy from manning.com. 

[Find out more]

Categories:

No Responses

Leave a Reply

Your email address will not be published. Required fields are marked *