Hands-on Arrow

Read-only version of a talk with the same name: slides, video

If you always wanted to try some fancy functional programming and/or Arrow, then it’s a right place to start.

Disclaimer: this article is intro-level and covers NonEmptyList, Either and basics of Monad comprehensions. Some definitions may be simplified for a better understanding.

What is Arrow?

Arrow is actually not a single library but four:

Sounds both exciting and scary? Don’t worry, we are covering only Core (and a bit of FX) today. :)

Let’s start with something simple…

NonEmptyList

Imagine, you want to work this structure:

Name Type   Is required?
Payments List of Items(Phone, Reference, Amount)    
Phone String(100) Required
Reference String(100) Required
Amount String(100) Required

But does the following JSON

{
  "payments" : []
}

make sense?

From the development standpoint sure, why not? An empty list is a valid List. But in real life it's more complicated. To confirm payments without any payment sounds wrong.

Let’s restrict it on the type level:

NonEmptyListOf() // doesn't compile

NonEmptyList(1, 2, 3, 4, 5) // NonEmptyList<Int>

To add a smooth integration with Jackson use Arrow Integrations Jackson Module

Now our domain-specific logic is reflected in our code base. Sounds easy, isn’t it?

Time to move to a bit more scary this called Either

Either

But let’s take a step back to Java

Java Exceptions

Take a look on typical usage of an exception in Java:

void testFun() throws SomeException {
  if (somethingWrong) throw new SomeException
}  

We just mark a function with throws and deal with it later for example like this:

try {
  testFun()
} catch (SomeException e) {
  // handle
}

There’s nothing wrong with try-catch it’s just not always compatible with some APIs/styles. For example, the Stream API.

We simply cannot write something like this:

IntStream.range(0, 10).forEach(i -> testFun())

Because as we defined above testFun() the function is throwing a checked exception. And it has to be checked e.g. a common approach is to delegate the exception handling to somewhere else via unchecked exception:

IntStream.range(0, 10).forEach(hideException(i -> testFun()));

And then unfold it back:

unhideSomeException(() -> 
  IntStream.range(0, 10)
    .forEach(
      hideException(i -> 
        testFun()
      )
    )
);

Where “unhide” function should have some matcher inside to “retrieve” the hidden exception. If you can. The checked part of testFun() would be overwritten by hideException() and it depends on you how would you handle that.

Now the Stream API sounds not so fun. But aren’t we using Kotlin?

Kotlin Exceptions

In Kotlin, as in other popular JVM languages (e.g. Groovy, Scala), only unchecked exceptions are thrown during the runtime. So, we are not “forced” to have such checks.

Now the example above would be just

(0 until 10).forEach { testFun() }

(It’s better to use repeat though)

But the thing is, we still want to handle error-flows somehow. What do we have in Kotlin for that?

Nulls

Null is easy to use and also Kotlin standard library has a lot of built-in functions to use, like fun String.toIntOrNull(): Int? which returns a nullable type. The downsides are, it’s not so expressive (Why is null here? What exactly went wrong? Because of that, debugging may be complicated) and explicit null-checks are needed.

Result Type

Among built-in options Kotlin has a result type:

inline class Result<out T> : Serializable

No info about errors on compile time but at least we can do chains.

Compare the try-catch flow here:

fun deserialize(): Something {
   return try {
     doSomething()
   } catch (_: Exception) {
     try {
       doSomethingElse()
     } catch(_: Exception) {
       doSomethingElseAgain()
  }
}

And the chained one:

fun deserialize(): Something {
   return runCatching {
       doSomething()
   }.recoverCatching {
       doSomethingElse()
   }.getOrElse {
       doSomethingElseAgain()
   }
}

(Just be mindful about what exactly you can catch)

Sealed Type

If you can list every single outcome it makes sense to try to describe a sealed class. Imagine, you want to trade some stocks. You will either sell them successfully or something may go wrong due to full moon (or whatever belief of misfortune there’s in your culture) or Elon Musk crashed the market by tweeting something:

sealed class Result

class Success : Result()

class FullMoonFailure : Result()
class ElonMuskTweetedFailure : Result()
Either

As you can see in previous example, there’s only one condition for “success” but multiple reasons to “fail”. And that’s pretty typical everywhere. The idea of grouping by “single success” and “failure(s)” is the main concept of the Either class:

sealed class Either<out A, out B>

It has two subclasses called Left and Right. The first one is used to store data in case something went wrong and the second one is used in case of success. Just like this:

> Either.Left("Something went wrong")
> Either.Right(5)
Validated

Validated looks almost the same as Either class: sealed class Validated<out A, out B> but it’s used more to describe cases when you would like to collect the result of all validations and not the first one that failed. Take an example:

User(phone,  name,        age)
User(“test”, XÆA-12 Musk, -10)

Conceptually like this:

(You should never validate names though (classical read about Falsehoods Programmers Believe About Names))

Monad comprehensions

Again, let’s take a step back and take a look on the standard library.

Required/Contracts

A contract is a way to tell the compiler about our expectations. As usual, we don’t like nulls and let’s say we don’t like to work with them:

requireNotNull(request.userId)
requireNotNull(request.transactionId)

Inside, this can look like this:

public inline fun <T : Any> requireNotNull(value: T?): T {
    contract {
        returns() implies (value != null)
    }
    return requireNotNull(value) { "Required value was null" }
}

Source

If value was indeed null IllegalArgumentException will be thrown.

But as we discussed before sometimes we don’t really want Exceptions. We could check and handle each case step-by-step and build our own exception response.

if(request.userId == null) {
  return buildExceptionResponse("userId is null")
}
if(request.transaction.id == null) {
  return buildExceptionResponse("transaction id is null")
}
// and so on for any further parameters

Great, now we got some repetitive patterns. Is there anything that may help us?

Monads

Now, back to the topic. On a basic level, you may imagine that a monad is a class with map() and flatMap() methods. (It’s actually a bit more complicated than that, but it’s outside of the article’s scope)

See docs: map, flatmap

And that’s it.

I’m pretty sure you already know some of them! Some examples are:

Let’s rewrite our contract in that way so it returns Either:

private fun <T : Any> checkForNull(param: T?, paramName: String): Either<ErrorResponse, T> { // mapping "left" and "right" results }

And then we can chain multiple checks using flatMap:

val response = checkForNull(response)
    .flatMap { userId ->
        checkForNull(userId).flatMap { paymentId ->
            checkForNull(paymentId).flatMap { items ->
                checkForNull(items).flatMap { status ->
                    checkForNull(status).map {
                        (  )
                    }
                }
            }
        }
    }

And now magic begins! Since it adds more and more indentation, let’s rewrite it to be more readable:

val response = Either.fx {
    val userId = checkForNull(request.userId).bind()
    val paymentId = checkForNull(request.transaction.id).bind()
    val items = checkForNull(request.transaction.items).bind()
    val status = client.checkPaymentStatus(userId, )
}
return response.getOrHandle { it }

What bind() does is return the expression on the right side or stops the execution returning the left value and fx creates the context to use the bind() function.

Now you and your code are amazing~~~

Afterword

That’s it for today. I hope this gives you an idea of what you can adapt in your project (at least the NonEmptyList shouldn’t be scary!).

What would be your next steps?


Written by Karin-Aleksandra Monoid