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?
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?