1. Overview
Arrow is a library merged from KΛTEGORY and funKTionale.
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 about 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>0.7.3</version> </dependency>
3. Functional Data Types
Let’s start by investigating the data types in the core module.
3.1. Introduction to Monads
Some of the discussed data types here are Monads. Very 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 on Monads maybe a bag of peanuts can help.
Now let’s see the first data type from the arrow-core module.
3.2. Id
Id is the simplest wrapper in Arrow.
We can create it with a constructor or with a factory method:
val id = Id("foo") val justId = Id.just("foo");
And, it has an extract method to retrieve the wrapped value:
Assert.assertEquals("foo", id.extract()) Assert.assertEquals(justId, id)
The Id class fulfills the requirements of the Monad pattern.
3.3. 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 = Option.just(42) val constructor = Option(42) val emptyOptional = Option.empty<Integer>() 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 } }
3.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.
3.4. Eval
Eval is a Monad designed to control the evaluation of operations. It has a 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)
4. Error Handling Patterns with Functional Data Types
Error handling by throwing exceptions has several drawbacks.
For methods which 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 find out 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
4.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 input.
Now, let’s look at how we can provide a more detailed description of an error case with Either.
4.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, 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> = Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> 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!" } } }
5. 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.