Functional Programming with Kotlin and Arrow: Getting Started
In this tutorial, you will learn the fundamentals of functional programming and how various Kotlin language features enable functional programming concepts. By Massimo Carli.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Functional Programming with Kotlin and Arrow: Getting Started
30 mins
- Getting Started
- OOP in a Nutshell
- Types as Abstractions
- Abstracting Identity
- Collaboration
- Immutability
- Moving From Objects to Functions
- Defining Functions
- Function Types
- Applying Higher-Order Functions
- Functions in Kotlin: Considering Special Cases
- Special Case #1: A Function With Nothing
- Special Case #2: The Unit Function
- Special Case #3: Predicate Functions
- Function Composition
- Side Effects
- Pure Functions
- Logger: The FP Way
- Applying Abstraction
- Implementing Composition
- What About Arrow?
- Where to Go From Here?
Fundamentals are, of course, fundamental — and fun! This is particularly true for functional programming (FP). You may already use FP in your Kotlin code. If you’re curious about what’s behind this important paradigm, this is the tutorial for you.
This functional programming tutorial dives deeper into the following topics:
- Function types
- Side effects
- Higher-order functions
- Abstraction and composition
- The Arrow framework
Writing some code examples, you’ll learn how to “think functionally” and how FP can make your code more testable and safe. In order to go fast, we need to go well so let’s start!
Getting Started
First, click the Download Materials button at the top or the bottom of the page. You’ll see the initial and final code, but you’ll write most of it during this tutorial. Open the starter project in IntelliJ IDEA. In the Books.kt file, you’ll find the definition of the Book
and Price
classes:
data class Price(val value: Double, val currency: String = "$")
data class Book(
val ISDN: String,
val name: String,
val pages: Int,
val price: Price,
val weight: Double,
val year: Int,
val author: String
)
You’ll also find a list of instances used to initialize the books
property.
Just as Object Oriented Programming (OOP) means working with objects, Functional Programming (FP) means working with functions. This is the start of your journey from OOP to FP. Fasten your seatbelt!
OOP in a Nutshell
The main concept in OOP is the class because it’s the first construct that you can use to describe the makeup of objects — instances of that class — in terms of properties and operations.
To start, open Books.kt. Now, to create an instance of the Book
class, invoke one of its constructors and assign the returned reference to an androidBook
variable in main()
:
val androidBook = Book(
"8850333404",
"Android 6: guida per lo sviluppatore (Italian Edition)",
846,
Price(39.26, "£"),
2.1,
2016,
"Massimo Carli"
)
Note how Book
is not just a class but also defines a type. Book
is a type of androidBook
variable that can reference any other object of the same type. While an object is an instance of a specific class, its reference can be assigned to a variable of different types. Add this code at the end of main()
:
val obj: Any = androidBook
println("obj description: $obj")
This assignment is possible because a Book
is an extension of Any
. Stated another way, Any
is an abstraction of Book
.
Build and run. You should see the description for obj
in the Run window.
obj name: Book(ISDN=8850333404, name=Android 6: guida per lo sviluppatore (Italian Edition), pages=846, price=Price(value=39.26, currency=£), weight=2.1, year=2016, author=Massimo Carli)
Types as Abstractions
Abstraction is the most important concept in software development. An alternative name for abstraction is subtraction. In this context, abstracting means including only what’s necessary.
The variable androidBook
in the previous code is of the type Book
because you’re interested in its properties such as name
or price
. When you use the obj
variable of type Any
, it means that you don’t care about the book properties. You just care about the fact that it is an object.
Abstracting Identity
Abstraction makes one object equal to another. A lion isn’t a tiger, but they’re equal if you think of them as carnivorous animals. In the same way, think of a type of a variable as a way of representing all the possible values that the same variable can reference.
Add the following code to main()
:
val myBook: Book = androidBook
println("myBook description: $myBook")
Here myBook
is a Book
which means it can contain, or reference, any element in the set of all possible books. A type, then, is a way to represent a set of values. A class describes the makeup of all its instances, and the type abstracts the set of all of them.
Build and run. The descriptions for obj
and myBook
match as expected.
obj description: Book(ISDN=8850333404, name=Android 6: guida per lo sviluppatore (Italian Edition), pages=846, price=Price(value=39.26, currency=£), weight=2.1, year=2016, author=Massimo Carli)
myBook description: Book(ISDN=8850333404, name=Android 6: guida per lo sviluppatore (Italian Edition), pages=846, price=Price(value=39.26, currency=£), weight=2.1, year=2016, author=Massimo Carli)
Collaboration
In creating a program, you define how objects interact with each other. Generally, objects are instances of classes you design for them to collaborate.
One of the main benefits of OOP is encapsulation, in which objects interact using their interfaces, or a set of operations that other objects see and can invoke. Unfortunately, objects are not very good in a multithreaded environment because they encapsulate what really matters — the mutable state.
Objects hide the way they mutate their states and, because they collaborate, they share data. Mutable and shared states are the primary cause of data races, which often result in bugs.
Immutability
A data race happens when you have multiple threads accessing the same mutable state in an unsafe way. Even without FP, it’s possible to solve this problem by removing one of the causes.
A data race can’t exist if you have only one thread. Many systems use this Single Thread Model for UI management, allowing a single main thread — in this case, the UI thread — to access UI components.
Another option is not to allow mutable states. This is the case when you create immutable objects, which are thread-safe because they can’t change their state after creation.
Using immutable objects is a good practice in both OOP and FP. Your Book
class is already immutable because of val instead of var in the definition of its properties.
To provide a mutable version of the same class, make it explicit defining the MutableBook class. Add the following code to Books.kt:
data class MutableBook(
var ISDN: String,
var name: String,
var pages: Int,
var price: Price,
var weight: Double,
var year: Int,
var author: String
)
Moving From Objects to Functions
You might have some key questions about moving from objects to functions:
- What’s the equivalent of a class in an FP world?
- What’s the equivalent of an instance?
- Can you use the same logical path you used with classes and objects?
- What about types, abstraction and collaboration? Can a function be the solution to the data race problem you face in OOP?
Read on for the answers to these questions!
Defining Functions
A function receives data as input and produces output, which may or may not depend on the input. Picture a coffee machine. The input is water and coffee beans, and the output is coffee:
A function can be something that receives a book
as input and returns its price. More importantly, a function is a way to map items of one type into items of another type. Kotlin isn’t an FP language, but it contains different features that allow you to use many FP concepts.
Next, create a Functions.kt file and add the function below, which can be used to return the weight of a Book
:
fun bookWeight(book: Book) = book.weight
In the same way you can create a function that returns the price of a Book
. Add this code to Functions.kt:
fun bookPrice(book: Book) = book.price.value
In Kotlin, functions are first-class citizens and can be treated as data. This means that you can define the previous functions like this:
val bookWeightFun = fun(book: Book) = book.weight
val bookPriceFun = fun(book: Book) = book.price.value
Because functions are like objects, you can even assign them to a variable. Add the main()
function below to the Functions.kt file:
fun main() {
// 1
var bookFun = bookWeightFun
println("Book weight: ${bookFun(books[0])} Kg")
// 2
bookFun = bookPriceFun
println("Book price: ${bookFun(books[0])} £")
}
Breaking down the above:
- The
bookFun
function variable is initialized using thebookWeightFun
variable. Using the first book from the collection in Books.kt,bookFun
will print out the book weight. - Reassign
bookFun
to thebookPriceFun
function and notice the price will now be printed.
Build and run. You’ll get an output like this:
Book weight: 2.1 Kg
Book price: 39.26 £
Function Types
When you define a variable, you’re implying that it can change its value. In the following code, you define the anotherBook
variable, which can reference any instance of the Book class. The type of anotherBook
is then Book. Add this code to a new file named FunctionTypes.kt:
var anotherBook = Book(
"8850330731",
"Android 3: Guida per lo sviluppatore (Italian Edition)",
642,
Price(40.06, "£"),
1.8,
2011,
"Massimo Carli"
)
You can do the same with a function. In the following code, you define the anotherBookFun
variable, which references the function you created earlier:
var anotherBookFun = fun(book: Book) = book.weight
You know that the anotherBookFun
variable can also reference any other function of the same type. So, what is the type of a function?
Again, a variable’s type is a way to abstract all the possible values the variable can reference. In this case, those are functions like anotherBookFun
and the previous bookFun
. This is like a pipe with an input of type Book and an output of type Double. With Kotlin you can represent this type using this typealias
definition. Add the following code:
typealias BookMapper<T> = (Book) -> T
This is a fundamental concept: The type of a function depends on the input and output types. In other words, what defines the type of a function is the set of possible values in input and the set of all the possible values in the output.
More importantly, the type of a function has nothing to do with how the mapping from the input and output value is done. That’s where the abstraction enters into play and where it becomes clear that FP has its own polymorphism.
To see this in action, copy this code into the FunctionTypes.kt file:
fun main() {
// 1
var mapper: BookMapper<Double> = ::bookWeight
// 2
var currency: BookMapper<String> = { book -> book.price.currency }
// 3
println("Weight of ${books[0].name} is ${mapper(books[0])} Kg")
// 4
mapper = ::bookPrice
// 5
println("Price of ${books[0].name} is ${mapper(books[0])}${currency(books[0])}")
}
Here’s what’s happening in the code above:
- Create a mapper variable, which references the function, which returns the weight of a book.
- The currency variable defines a function, which returns the currency for the given book as a string.
- Print the name of a book with its weight using the previously defined mapper variable.
- Update the mapper variable so it now refers to the function for the price of a book.
- Print the name and price of a book using the updated mapper variable.
Build and run. You’ll get an output like this:
Weight of Android 6: guida per lo sviluppatore (Italian Edition) is 2.1 Kg
Price of Android 6: guida per lo sviluppatore (Italian Edition) is 39.26£
Applying Higher-Order Functions
In the previous code, you assigned functions to variables as though they were normal objects. This is important to note because a function can be a parameter of another function or a return value. Kotlin developers are familiar with code like this, which can be added to a new HighOrderFunction.kt file:
fun List<Book>.total(fn: BookMapper<Double>): Double =
fold(0.0) { total, book -> total + fn(book) }
This creates an extension function for the List<Book> that accepts as input a function that maps a book to the value to add to the total of type Double. This is a typical example of a function that accepts another function as a parameter. To understand how it works, add the following code into the HighOrderFunction.kt file:
fun main() {
// 1
val totalPrice = books.total { it.price.value }
val totalWeight = books.total { it.weight }
// 2
println("Total Price: ${totalPrice} £")
println("Total Weight: ${totalWeight} Kg")
// 3
books.forEach { println(it.name) }
}
Here, you:
- Use a lambda expression, pass the function parameter to the total function and calculate the total price and total weight of a list of books.
- Print the totals.
- Use a predefined higher order function of Kotlin in order to print all the names of the list of books.
Higher-order functions allow us to answer the first question from the section on FP: When you program using OO, you define classes. When you use FP, you create higher-order functions.
Build and run. You’ll get an output like this:
Total Price: 212.06 £
Total Weight: 9.700000000000001 Kg
Android 6: guida per lo sviluppatore (Italian Edition)
Android 3: Guida per lo sviluppatore (Italian Edition)
Sviluppare applicazioni Android con Google Play services (Italian Edition)
Creare la prima applicazione Android - Kindle
Android 4: Guida per lo sviluppatore (Italian Edition)
RoboGuice e Robotium: Dependency Injection applicata ad Android - Kindle
Android Activity: Gestire il flusso di navigazione di un'app - Kindle
Sviluppare applicazioni per Android (Italian Edition)
Functions in Kotlin: Considering Special Cases
A function defines a way to map values from an input set to values of an output set. For instance, the type BookMapper<T>
abstracts all the possible functions that map Book objects into instances of type T. These sets are theoretically infinite but numerable with some special cases:
- The empty set
- A set with a single value
- A set with two values
Special Case #1: A Function With Nothing
An empty set contains no elements. For the moment, call the related type Empty. You can define a function type that maps values from Empty to and from any other types T:
typealias FromEmpty<T> = (Empty) -> T
typealias ToEmpty<T> = (T) -> Empty
typealias EmptyId = (Empty) -> Empty
When dealing with the Empty, problems may arise. Consider this function definition:
fun absurd(value: Empty): String = "I'm absurd"
The name of this function is absurd because it’s a function you can’t invoke. To use it, you need a value for the parameter of type Empty. A type corresponds to a set of values, but in this case, the set is empty, and it has no values.
You can define a function that maps values of type T to values of type Empty, but it can’t return any values for the same reason.
At this point, you should recognize the Nothing type. Its source code should show that it represents a type but not a class, in the general sense, because you can’t instantiate it:
public class Nothing private constructor()
Special Case #2: The Unit Function
Another special case happens when the set contains only one element. This type can be used for the input or the output, and in Kotlin corresponds to the Unit type. Unit is not just a type, it’s also the single existing instance for that type. Look at this example:
typealias unit<T> = (T) -> Unit
This code abstracts all the possible functions that receive a generic type T as input and returns the only existing object of the Unit type, which is the Unit value. This is the unit function.
It’s interesting how these types of functions are mapped into Java functions, which return void. In Kotlin, they’re returning something, but it’s always the same instance unit. For proof, look at the source code where it’s implemented as a singleton:
public object Unit {
override fun toString() = "kotlin.Unit"
}
Special Case #3: Predicate Functions
What about a set with only two values? You already know the type for this special set: it’s Boolean
. A function that maps values to this set is a Predicate, represented like this:
typealias Predicate<T> = (T) -> Boolean
Now that you have a handle on the basics of abstraction, consider the concept of composition.
Function Composition
In OOP, objects interact with each other using their interfaces. Collaboration between functions is called Composition.
Create a Composition.kt file and add this type definition, which represents every possible function from A to B:
typealias Func<A, B> = (A) -> B
Then, write:
val getPrice: Func<Book, Price> = { book -> book.price }
val formatPrice: Func<Price, String> =
fun(priceData: Price) = "value: ${priceData.value}${priceData.currency}"
The former, of type Func<Book,Price>
returns the price of a book, and the latter, of type Func<Price,String>
, provides a formatted version of the same price. The output type of the former is the same as the input type of the latter.
Create another function called after whose type is Func<Book, String>
, which invokes the function formatPrice
after the function getPrice
like this:
infix fun <A, B, C> Func<B, C>.after(f: Func<A, B>): Func<A, C> = { x: A -> this(f(x)) }
This invokes a function by passing the output value of the previous function as a parameter. In other words, it implements function composition. You can then compose the two functions in two different ways:
fun main() {
// 1
val result: String = formatPrice(getPrice(books[0]))
println(result)
// 2
val compositeResult: String = (formatPrice after getPrice)(books[0])
println(compositeResult)
}
This demonstrates an important difference:
- Invoke the formatPrice function using the output of the getPrice function.
- Invoke the function that is the composition of the formatPrice and getPrice functions.
Build and run. You’ll get an output like this which proves the equivalence of the two expressions:
value: 39.26£
value: 39.26£
Side Effects
Remember, you’re studying functions because you had a data race problem to solve. To understand how functions can solve the data race problem, create a new Pure.kt file with this code:
class Logger {
var log = StringBuilder()
fun log(str: String) {
log = log.append(str).append("\n")
}
}
This is a Logger
class, whose responsibility is to log operations. Each time you invoke the log
method, you append a new line.
Suppose you want to log the operation for the functions getPrice
and formatPrice
that you created in the previous section. You might write:
val logger = Logger()
val getPriceWithLog: Func<Book, Price> = {
logger.log("Price calculated for ${it.ISDN}")
it.price
}
val formatPriceWithLog: Func<Price, String> = {
logger.log("Bill line created")
"value: ${it.value} ${it.currency}"
}
Here, you created a Logger
instance to use in the function bodies. Hey! You’re using functions, but you still have a shared resource with a shared state — the log. You can see this by adding and running the main
function below:
fun main() {
formatPriceWithLog(getPriceWithLog(books[0]))
println(logger.log)
}
Running this code you get:
Price calculated for 8850333404
Bill line created
When you invoke the getPriceWithLog
or formatPriceWithLog
functions, you’re changing the log and upsetting the world where other functions can run. This is called a side effect. So not all functions are a solution to the data race problem in a multithreaded environment.
Even worse: What if the output of the function itself depends on the side effect? How can you test these functions? You need something more.
Pure Functions
Not all functions are good. Some of them still share data, and sharing data doesn’t work in a multithreaded environment. However, you can define a function that doesn’t perturb the environment where it’s executed. For that, you need a pure function, which is a function with no side effects.
Pure functions have outputs that depend on their input. Repeatedly invoking a pure function with the same input always yields the same output. They don’t share a state, and they have everything they need in their parameters.
Pure functions are like a lookup table. They are data, and you can replace the function invocation with its output. This is referential transparency. It’s useful because it allows the compiler to perform optimizations, which wouldn’t be possible otherwise.
Some languages, like Haskel, force you to define pure functions, but Kotlin is more flexible. How can you fix the previous logger problem, then?
Logger: The FP Way
In the previous code, you used functions that were not pure with a shared state and a side effect. How can you make the same functions pure and safe in a multithreaded environment?
Everything a pure function does must use its parameters, so the logger version of your example must be one of those. Think of FP as a superhero with two special powers: abstraction and composition. This code is all you need:
Applying Abstraction
Start a new file with the name FPLogger.kt and define this first abstraction:
typealias Writer<T, R> = (T) -> Pair<R, String>
The Writer
type abstracts functions from a generic type T to a type Pair<R, String>, where the second parameter is the content of the log to append. This is very powerful because you can now write these functions:
val fpGetPrice: Writer<Book, Price> =
fun(book: Book) = getPrice(book) to "Price calculated for ${book.ISDN}"
val fpFormatPrice: Writer<Price, String> =
fun(price: Price) = formatPrice(price) to "Bill line created for ${formatPrice(price)}"
They are now functions of the same generic type Writer<T,R>
.
Implementing Composition
Now you need to define what composition means for your new abstraction. Define the compose function like this:
infix fun <A, B, C> Writer<A, B>.compose(f: Writer<B, C>): Writer<A, C> =
{ x: A ->
val p1 = this(x)
val p2 = f(p1.first)
p2.first to p1.second + "\n" + p2.second
}
Think of the type Writer<T, R>
as the union of two different functions. The first maps elements in T into elements in R. The second maps the result of type R into a String. The first component of the resulting type is then the normal composition of two functions. The second component is the composition of two strings.
Test the result using this code:
fun main() {
// We compose the functions
val getPriceWithLog = fpGetPrice compose fpFormatPrice
books.forEach { book ->
println(getPriceWithLog(book).second)
}
}
Build and run getting an output like this:
Price calculated for 8850333404
Bill line created for value: 39.26£
Price calculated for 8850330731
Bill line created for value: 40.06£
Price calculated for 885033334X
Bill line created for value: 34.03£
Price calculated for B00BVBVSZ8
Bill line created for value: 3.99£
Price calculated for 885033222X
Bill line created for value: 55.2£
Price calculated for B00FED9XQ0
Bill line created for value: 4.49£
Price calculated for 885033222X
Bill line created for value: 4.49£
Price calculated for 8850330103
Bill line created for value: 30.54£
What About Arrow?
Category Theory inspired this tutorial series in order to promote functional programming. That is also the motivation for the creation of the Arrow library, which defines a set of APIs to apply FP concepts like the composition you saw earlier.
You’ll study many Arrow constructs in future tutorials, but as a first step, you can use composition though the compose function after adding these dependencies in the build.gradle file in the dependencies section:
def arrow_version = "0.9.0"
compile "io.arrow-kt:arrow-core:$arrow_version"
compile "io.arrow-kt:arrow-syntax:$arrow_version"
Now, use the compose function to rewrite the example from Composition.kt in a new Arrow.kt file:
import arrow.core.compose
fun main() {
val compositeResult: String = (formatPrice compose getPrice)(books[0])
println(compositeResult)
}
If you build and run you get this output which proves how the Arrow’s compose function works:
value: 39.26£
Where to Go From Here?
Now that you have a deep understanding of the fundamentals of FP, abstraction and composition, you can start playing with the Arrow framework. Use the Download Materials button at the top or bottom of the tutorial to access the final version of the code.
This is just the beginning! Keep practicing. Check out Bartosz Milewski’s video Category Theory for Programmers course for more details on category theory.
If you have any questions or comments, please join the discussion below!