1. Overview

Arrow is a library for Kotlin; it brings functional programming constructs to the language. Its goals are similar to what Cats Library does for Scala.

Arrow provides these constructs through two primary ways: extension functions and types.

Extension functions existing Kotlin and Java types with new functionalities without using inheritance. They act as a bridge, enabling more natural function invocation patterns and enhancing type scope with additional capabilities.

Arrow also provides Algebraic Data Types (ADTs) that implement the interfaces of functors, applicatives, and monads. For example, the Validated class in Arrow implements the Applicative interface.

In this tutorial, we’ll look at the basics of Arrow and how it can help us harness the power of functional programming in Kotlin.

We’ll discuss the data types in the core package and investigate a use case for error handling.

2. Maven Dependency

To include Arrow in our project, we have to add the arrow-core dependency:

<dependency>
    <groupId>io.arrow-kt</groupId>
    <artifactId>arrow-core</artifactId>
    <version>1.2.0</version>
</dependency>

3. Exploring Extension Functions in Arrow

Arrow library enriches Kotlin with extension functions designed to bolster functional programming. This section will cover some notable extension functions that underscore the library’s power and flexibility.

3.1. Basic Concepts: Composition, Currying, and Partial Application

Composing functions is a quintessential functional programming task; Arrow provides us with a compose() extension function that we can use to create a new function that takes the result of the first one and applies the second one:

val multiplyBy2 = { i: Int -> i * 2 }
val add3 = { i: Int -> i + 3 }
val composed = multiplyBy2 compose add3
val result = composed(4)  // Result: 14

Another interesting function is curried(), which allows us to take multiple parameters and convert them to a chain of functions, taking only one parameter that returns a function taking one parameter; it makes our functions more flexible and allows us to apply parameters to them in a step by step manner:

val add = { a: Int, b: Int -> a + b }
val curriedAdd = add.curried()
val add2 = curriedAdd(2)
val result = add2(3)  // Result: 5

Finally, partially() gives us the possibility to pass on some arguments to a function without having to curry it first,  creating a function with fewer parameters but not converted to a chain of single parameter functions:

val addThreeNums = { a: Int, b: Int, c: Int -> a + b + c }
val add2 = addThreeNums.partially1(2)
val result = add2(3, 4)  // Result: 9

3.2. Extending Existing Types With  flatMap() and flatten()

Arrow also provides implementations of classic Functional Programming methods for Kotlin or even Java types, Arrow uses extension functions to add flatMap() to some Arrow types but also to the Result and Map Kotlin types:

// Define a Map
val myMap = mapOf(
    "a" to listOf(1, 2, 3),
    "b" to listOf(4, 5, 6),
    "c" to listOf(7, 8, 9)
)

// Use flatMap to transform and flatten the Map
val flatMappedList = myMap.flatMap { entry ->
    entry.value.map { value ->
        "$${entry.key}$value"
    }
}

// Output: listOf("a1", "a2", "a3", "b4", "b5", "b6", "c7", "c8", "c9")
println(flatMappedList)

The flatten() function extends some containers (E.g. Iterable, Sequence), with a function that enables us to simplify nested contexts into a single level:

val list = listOf(listOf(1, 2), listOf(3, 4))
val result = list.flatten()  // Result: listOf(1, 2, 3, 4)

4. Functional Data Types

Let’s start by investigating the data types in the core module.

4.1. Introduction to Monads

Some of the discussed data types here are Monads. Basically, Monads have the following properties:

  • They are a special data type that is basically a wrapper around one or more raw values
  • They have three public methods:
    • a factory method to wrap values
    • map
    • flatMap
  • These methods act nicely, that is, they have no side-effects.

In the Java world, arrays and streams are Monads, but Optional isn’t. For more about Monads, we could review this series explaining it progressively from ADTs to Monads.

Now let’s see the first data type from the arrow-core module.

4.2. Option

Option is a data type to model a value that might not be present, similar to Java’s Optional.

And while it isn’t technically a Monad, it’s still very helpful.

It can contain two types: The Some wrapper around the value or None when it has no value.

We have a few different ways to create an Option:

val factory = Some(42)
val constructor = Option(42)
val emptyOptional: Option<Integer> = none()
val fromNullable = Option.fromNullable(null)

Assert.assertEquals(42, factory.getOrElse { -1 })
Assert.assertEquals(factory, constructor)
Assert.assertEquals(emptyOptional, fromNullable)

Now, there is a tricky bit here, which is that the factory method and constructor behave differently for null:

val constructor : Option<String?> = Option(null)
val fromNullable : Option<String?> = Option.fromNullable(null)
Assert.assertNotEquals(constructor, fromNullable)

We prefer the second since it doesn’t have a KotlinNullPointerException risk:

try {
    constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
    fromNullable.map { s -> s!!.length }
}

4.3. Either

As we’ve seen previously, Option can either have no value (None) or some value (Some).

Either goes further on this path and can have one of two values. Either has two generic parameters for the type of the two values, which are denoted as right and left:

val rightOnly : Either<String,Int> = Either.Right(42)
val leftOnly : Either<String,Int> = Either.Left("foo")

This class is designed to be right-biased. So, the right branch should contain the business value, say, the result of some computation. The left branch can hold an error message or even an exception.

Therefore, the value extractor method (getOrElse) is designed toward the right side:

Assert.assertTrue(rightOnly.isRight())
Assert.assertTrue(leftOnly.isLeft())
Assert.assertEquals(42, rightOnly.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.getOrElse { -1 })

Even the map and the flatMap methods are designed to work with the right side and skip the left side:

Assert.assertEquals(0, rightOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertTrue(rightOnly.flatMap { Either.Right(it % 2) }.isRight())
Assert.assertTrue(leftOnly.flatMap { Either.Right(it % 2) }.isLeft())

We’ll investigate how to use Either for error handling in section 4.

4.4. Eval

Eval is a Monad designed to control the evaluation of operations. It has built-in support for memoization and eager and lazy evaluation.

With the now factory method, we can create an Eval instance from already computed values:

val now = Eval.now(1)

The map and flatMap operations will be executed lazily:

var counter : Int = 0
val map = now.map { x -> counter++; x+1 }
Assert.assertEquals(0, counter)

val extract = map.value()
Assert.assertEquals(2, extract)
Assert.assertEquals(1, counter)

As we can see, the counter only changes after the value method is invoked.

The later factory method will create an Eval instance from a function. The evaluation will be deferred until the invocation of value, and the result will be memoized:

var counter : Int = 0
val later = Eval.later { counter++; counter }
Assert.assertEquals(0, counter)

val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)

val secondValue = later.value()
Assert.assertEquals(1, secondValue)
Assert.assertEquals(1, counter)

The third factory is always. It creates an Eval instance which will recompute the given function each time the value is invoked:

var counter : Int = 0
val later = Eval.always { counter++; counter }
Assert.assertEquals(0, counter)

val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)

val secondValue = later.value()
Assert.assertEquals(2, secondValue)
Assert.assertEquals(2, counter)

5. Error Handling Patterns With Functional Data Types

Error handling by throwing exceptions has several drawbacks.

For methods that fail often and predictably, like parsing user input as a number, it’s costly and unnecessary to throw exceptions. The biggest part of the cost comes from the fillInStackTrace method. Indeed, in modern frameworks, the stack trace can grow ridiculously long with surprisingly little information about business logic.

Furthermore, handling checked exceptions can easily make the client’s code needlessly complicated. On the other hand, with runtime exceptions, the caller has no information about the possibility of an exception.

Next, we’ll implement a solution to determine if the even input number’s largest divisor is a square number. The user input will arrive as a String. Along with this example, we’ll investigate how Arrow’s data types can help with error handling

5.1. Error Handling With Option

First, we parse the input String as an integer.

Fortunately, Kotlin has a handy, exception-safe method:

fun parseInput(s : String) : Option<Int> = Option.fromNullable(s.toIntOrNull())

We wrap the parse result into an Option. Then, we’ll transform this initial value with some custom logic:

fun isEven(x : Int) : Boolean // ...
fun biggestDivisor(x: Int) : Int // ...
fun isSquareNumber(x : Int) : Boolean // ...

Thanks to the design of Option, our business logic won’t be cluttered with exception handling and if-else branches:

fun computeWithOption(input : String) : Option<Boolean> {
    return parseInput(input)
      .filter(::isEven)
      .map(::biggestDivisor)
      .map(::isSquareNumber)
}

As we can see, it’s pure business code without the burden of technical details.

Let’s see how a client can work with the result:

fun computeWithOptionClient(input : String) : String {
    val computeOption = computeWithOption(input)
    return when(computeOption) {
        is None -> "Not an even number!"
        is Some -> "The greatest divisor is square number: ${computeOption.t}"
    }
}

This is great, but the client has no detailed information about what was wrong with the input.

Now, let’s look at how to provide a more detailed description of an error case with Either.

5.2. Error Handling With Either

We have several options to return information about the error case with Either. On the left side, we could include a String message, an error code, or even an exception.

For now, we create a sealed class for this purpose:

sealed class ComputeProblem {
    object OddNumber : ComputeProblem()
    object NotANumber : ComputeProblem()
}

We include this class in the returned Either. In the parse method we’ll use the cond factory function:

Either.cond( /Condition/, /Right-side provider/, /Left-side provider/)

So, instead of Option, we’ll use Either in our parseInput method:

fun parseInput(s : String) : Either<ComputeProblem, Int> =
  if(s.toIntOrNull() != null) Right(s.toInt()) else Left(ComputeProblem.NotANumber)

This means that the Either will be populated with either the number or the error object.

All the other functions will be the same as before. However, the filter method is different for Either. It requires not only a predicate but a provider of the left side for the predicate’s false branch:

fun computeWithEither(input : String) : Either<ComputeProblem, Boolean> {
    return parseInput(input)
      .filterOrElse(::isEven) { -> ComputeProblem.OddNumber }
      .map (::biggestDivisor)
      .map (::isSquareNumber)
}

This is because, we need to supply the other side of the Either, in the case our filter returns false.

Now the client will know exactly what was wrong with their input:

fun computeWithEitherClient(input : String) {
    val computeWithEither = computeWithEither(input)
    when(computeWithEither) {
        is Either.Right -> "The greatest divisor is square number: ${computeWithEither.b}"
        is Either.Left -> when(computeWithEither.a) {
            is ComputeProblem.NotANumber -> "Wrong input! Not a number!"
            is ComputeProblem.OddNumber -> "It is an odd number!"
        }
    }
}

6. Conclusion

The Arrow library was created to support functional features in Kotlin. We have investigated the provided data types in the arrow-core package. Then we used Optional and Either for functional style error handling.

As always, the code is available over on GitHub.

2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.