Functional Flair with Arrow: Elevate Your Kotlin Code with Efficient Error Handling

Thaw Zin Toe
4 min readFeb 12, 2023

Functional programming has been gaining increasing popularity in recent years, and for good reason. Its concise and expressive syntax provides a simple and effective solution for tackling complex problems. The popular programming language, Kotlin, offers excellent support for functional programming through its standard library and through a third-party library called Arrow.

Error handling is an essential aspect of software development, especially when it comes to mobile applications. In the Android ecosystem, error handling has its own set of challenges due to the nature of the platform.

This article will delve into the basics of Arrow functional programming and explore its applications in Kotlin.

What is Arrow?

Arrow is a library for Typed Functional Programming in Kotlin. Arrow is composed of 4 main modular libraries

  1. Core—a foundational library that provides the core abstractions, type classes, data structures, and base continuation effect systems (System Design), which includes patterns to remove callbacks and enables controlled effects in direct syntax.
  2. Fx — a library that provides functional, lightweight, and reactive frameworks for building applications which is more reactive and event-driven architecture, making it easier to build scalable and concurrent applications. You can combine it with kotlin flow in Android Development.
  3. Optics — provides an automatic DSL that allows users to use . notation when accessing, composing, and transforming deeply nested immutable data structures.
  4. Meta — is a general-purpose library for meta-programming in Kotlin to build compiler plugins. Some type system features proposed by Arrow such as union types, product types, proof derivation, and others are built with Arrow Meta and serve as examples of what could be incorporated into the Kotlin compiler.

Arrow-Core

In your project’s root build.gradle.kts, append this repository to your list:

allprojects {
repositories {
mavenCentral()
}
}

Add the dependencies to the project’s build.gradle.kts:

dependencies {
implementation("io.arrow-kt:arrow-core:1.1.2")
}

BOM file

To avoid specifying the Arrow version for every dependency, a BOM file is available:

dependencies {
implementation(platform("io.arrow-kt:arrow-stack:1.1.2"))
implementation("io.arrow-kt:arrow-core")
}

Arrow provides several core concepts that form the foundation of functional programming in Kotlin. Let’s discuss some of the most important ones:

  • Option: The Option type is used to represent optional values. It’s similar to Java’s Optional, but it’s more expressive and provides a cleaner syntax.
  • Either: Either type is used to represent values that can either be of one type or another. This type is particularly useful when you want to represent values that can either be a success or a failure.
  • Try: The Try type is used to represent computations that can either result in success or failure.
  • Validated: The Validated type is used to represent computations that can produce multiple failures.
  • IO: The IO type represents side-effecting computations that can be executed later.

Functional Error Handling

Functional error handling is a way for programmers to handle errors in a functional and organized way. Instead of using traditional methods like try-catch blocks, functional error handling uses something called monads. Monads are abstractions that separate the work of handling errors from the work of doing the actual computation.

It provides a more functional and organized way to handle errors in software systems, making it easier to build strong and scalable systems.

Requirements

/** model */
object Lettuce
object Knife
object Salad

fun takeFoodFromRefrigerator(): Lettuce = TODO()
fun getKnife(): Knife = TODO()
fun prepare(tool: Knife, ingredient: Lettuce): Salad = TODO()

Exceptions

fun takeFoodFromRefrigerator(): Lettuce = throw RuntimeException("You need to go to the store and buy some ingredients")
fun getKnife(): Knife = throw RuntimeException("Your knife needs to be sharpened")
fun prepare(tool: Knife, ingredient: Lettuce): Salad = Salad

Issues with Exception

Exceptions are often used in traditional error handling, but they also have some problems that make them less ideal for use in software systems. Some of these problems are:

  1. Slow Performance: Throwing and catching exceptions can be slow, as the process requires the creation of an exception object and the creation of a stack trace. (http://normanmaurer.me/blog/2013/11/09/The-hidden-performance-costs-of-instantiating-Throwables/)
  2. Unclear Error Handling: With exceptions, it can be unclear how errors will be handled, as the error handling logic is not always explicitly defined.
  3. Error Swallowing: Exceptions can be silently ignored, which can make it difficult to detect and debug errors in the system.

How do we model exceptional cases then?

Arrow and the Kotlin standard library provide proper datatypes and abstractions to represent exceptional cases.

Either

sealed class Either<out A, out B>

We’ll explore how to use the Arrow Core Either data type to handle errors in Android and provide examples of how to use this technique in your own applications.

First, let’s understand the basics of Arrow Core Either. It is a data type that is used to represent either a success or an error. In the Android context, Either type is used to wrap the result of an asynchronous operation, such as a network request or a database query.

Either type has two variants

  1. Left — used to represent an error
  2. Right —used to represent a success

Here is an example of how you can use Arrow Core Either to handle errors in Android:

fun fetchUserData(id: Int): Either<Error, User> {
return when (val result = repository.getUserData(id)) {
is Result.Success -> Either.Right(result.data)
is Result.Error -> Either.Left(Error("Fetching user data failed"))
}
}

fun updateUserData(user: User): Either<Error, Unit> {
return when (val result = repository.updateUserData(user)) {
is Result.Success -> Either.Right(Unit)
is Result.Error -> Either.Left(Error("Updating user data failed"))
}
}

fun handleUserData(id: Int, updatedData: User): Either<Error, User> {
return fetchUserData(id)
.flatMap { fetchedData ->
if (fetchedData.id == id) {
Either.Right(fetchedData)
} else {
Either.Left(
Error("Fetched data does not match with the provided id")
)
}
}
.flatMap { userData ->
updateUserData(userData.copy(data = updatedData.data))
.map { userData }
}
}

you can see more detail in Either use-cases — https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-either/

See you next time bye bye 👋 👋

Credits

Tutorial adapted from https://arrow-kt.io/docs/patterns/error_handling/

--

--