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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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:

  1. The empty set
  2. A set with a single value
  3. 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:

  1. Invoke the formatPrice function using the output of the getPrice function.
  2. 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?

Massimo Carli

Contributors

Massimo Carli

Author

Nick Winegar

Tech Editor

Nicole Hardina

Editor

Luke Freeman

Illustrator

Namrata Bandekar

Final Pass Editor

Eric Soto

Team Lead

Over 300 content creators. Join our team.