The Result Monad

Recently I’ve come to really appreciate the kotlin-result library as we’ve integrated it more and more deeply into the Cuvva Android stack. In this post, I’ll explain why we needed it, how it works and what problems it solves, and show some examples of how we use it at the end. A great many people have tried and failed to explain what a monad is, and this is my attempt at being added to the list.

Exceptions in programming are a way of life, and somehow or another you’ll need to handle them if you’re to build anything that works for more than a few minutes. At Cuvva we were lucky enough to be gifted several months to re-write much of our existing app, and one of the things we spent a long time thinking about was how exactly we handle things when the unexpected happens. We settled on kotlin-result after some deliberation, but we’re increasingly seeing the payoff - what follows is an explanation of why I’m so enamoured with this way of working, and why we chose this library in particular.

Exceptions and building rails

For many of us, handling exceptions has at one time or another been a complete afterthought. I can guarantee that we’ve all, at some point, been guilty of this:

try {
  return someOperationThatThrows()
} catch (Exception e) {
  // If you're feeling really responsible...
  e.printStackTrace()
}
return null

Such lackadaisical and unfortunately common error handling is one reason why the designers of Kotlin decided that it wasn’t even worth forcing callers to handle exceptions - Kotlin’s exceptions are unchecked, which is to say that the compiler doesn’t force you to explicitly handle error cases. This of course contrasts with Java where most exceptions are checked, and therefore must be handled for compilation to succeed. Even if your team is incredibly disciplined, there’s nothing stopping someone from simply assuming that a function in Kotlin will always succeed or always return a value, even if it explicitly declares that it can throw an exception.

Let’s imagine for a minute that either JetBrains did decide to make exceptions checked, or that you have the most diligent team on earth.

Many companies of a certain size have fairly deep stacks which bubble information up to the UI. For us, this broadly looks like so in the case of fetching some data from a REST endpoint:

graph LR; A[API service] --> B[Repository] B --> C[Usecase] C --> D[ViewModel]

At each layer you don’t particularly want to do a try/catch and map the error to something that’s useful in that particular layer, nor do you want to simply force the ViewModel to handle the exception at the last second and decide what to do. You certainly don’t want to pretend everything is fine and simply call a function that can explode, or worse: simply not notice that the API you’re calling can throw.

A typesafe approach

One approach to this is what’s sometimes referred to as programming on rails, which is to say that you force two different code paths depending on whether or not an operation was successful or it failed. You can think of this as building both the happy and the sad path, and an obvious candidate for this - in Kotlin at least - is sealed classes. An early iteration of this in our codebase was far from generic, and we ended up writing sealed class hierarchies for most return types:

sealed class FetchVehicleResponse

data class Success(val vehicle: Vehicle) : FetchVehicleResponse()
object Failure: FetchVehicleResponse()

There’s a couple of non-trivial downsides to this. Firstly, it rapidly gets super confusing, especially if you ensure that your data layer objects don’t make their way up to the presentation layer - you’re going to rapidly end up with a class explosion that’s extremely hard to follow for not a lot of benefit except extremely strict type-checking.

When you do hit these boundary layers, you end up writing a tonne of code which looks like this:

return when (val data = fetchVehicle()) {
  is Success -> data.mapToDomainSuccess()
  else -> data.mapToDomainError()
}

The rails are pretty clear here, but you eventually get sick of writing these statements over and over for little benefit. It does force you to explicitly handle errors, but surely there’s a more generic solution?

sealed class Response<T, R: Throwable> {

    data class Success<T>(val data: T) : Response<T, Nothing>()
    data class Failure(val throwable: Throwable?) : Response<Nothing, Throwable>()
}

We could just wrap out data in a Response. This fixes the class explosion problem, sure, but many of the underlying issues remain. Endless when statements, try/catch around the initial network call to map it in the first place. It’s not really a great solution. What’s worse is when you need to start nesting or combining these calls together:

return when (val vehicle = fetchVehicle()) {
  is Response.Success -> when (val policy = fetchInsurancePolicy(vehicle.numberPlate)) {
    is Response.Success -> // Return some type
    else -> // You get the idea
  }
  else -> data.mapToDomainError()
}

This rapidly becomes pretty hard to read and is just another flavour of callback hell. Is there a better way?

Yes: we could use a monad.

Enter the Monad

A monad is just a monoid in the category of endofunctors, what’s the problem?

Okay, hold up, this is absolutely terrifying.

Don’t panic

But actually, when you break down the problem you’re trying to solve, it becomes reasonably obvious as to what exactly a monad is and how it solves various problems, even if the actual implementation looks like generic-based hell to you. Let’s walk through it together.

Ultimately, we want to:

  • Wrap and invoke functions that might blow up and catch their errors
  • Be able to map from one success type to another across data boundaries, ie from domain to presentation
  • Be able to map errors from one type to another, perhaps converting a Throwable to something more useful to the presentation layer
  • Handle both success and failure whenever we want
  • Ideally, we’d be able to chain or combine a bunch of these operations together, and only handle the error state at the end of the computation

Conceptually what we’re looking for is a wrapper that contains a value, and this value may or may not be there. If the operation was a success, it’s present, if an error was thrown, it isn’t and there’s a Throwable there instead. Depending on what language you’re used to this is effectively an Either monad, Data.Either in Haskell, or a Result in Rust.

data class SimpleResult<T, E>(
  val data: T? = null,
  val error: E? = null
)

This is all a monad really is; it’s a box. That box might be empty, but until you open the box at the end, it kind of doesn’t matter what you do to it in the meantime. You should be able to call function after function to modify the data it’s wrapping, chain operations with other monads, and if anything fails at any step, you only find out at the end. You don’t have to handle the error until you require the value, not during each intermediate step. And this is hugely powerful and lets you write very elegant, expressive, and above all safe code.

It’s probably worth pointing out that this isn’t the mathematical definition of a monad, and I’m sure this oversimplification will draw the ire of some readers. It is however close enough to convey the core concept in my opinion, particularly for this usecase. Happy to hear other people’s input on this!

A simple example

To quantify this explicitly, I have an SimpleResult monad which contains a number, 10. I call map and multiply that number by 10, then I call map again and convert the value to a String: at the end I get the value "100".

graph LR; A[10] --> |"map { it * 10 }"| B[100] B --> |"map { it.toString() }"| C[''100''] C --> |"data"| D((''100'')) C --> |"error"| E[null]

In another SimpleResult monad, I have a value, "abcd". I call map and attempt to convert this to a number, but this throws an NumberFormatException. Crucially, I don’t have to deal with the error here. I add a couple more map operations; multiplying the number and then formatting it to currency. At the end of the chain I attempt to unwrap the value and I find I have nothing - just the exception that was thrown earlier.

graph LR; A["abcd"] --> |"map { it.toInt() }"| B[null] B --> |"map { it * 5 }"| C[null] C --> |"map { it.toCurrency() }"| D[null] D --> |"data"| E((null)) D --> |"error"| F[NumberFormatException] linkStyle 0 color:red;

What has happened here is that when one step of the chain fails, the rest of the chain is abandoned: only the error is present.

Monads, how do they work?

In this naive implementation, we can see how we might achieve this:

// T = initial data type
// E = error type
// U = updated data type

data class SimpleResult<T, E>(
  val data: T? = null,
  val error: E? = null
) {

  fun <T, E, U> SimpleResult<T, E>.map(
    transform: (T) -> U
  ): SimpleResult<U, E> {
    return when {
        // If the data exists, transform it
        data != null -> SimpleResult(transform(data))
        // If it doesn't, don't do anything, just return this monad
        else -> this
    }
  }
}

We also want to be able to chain these operations and avoid the callback hell (or it’s equivalent) that we saw earlier. This is easy to implement too; it’s identical to map except the function that we pass to andThen returns a new Monad, not just the new type:

// T = initial data type
// E = error type
// U = updated data type

fun <T, E, U> SimpleResult<T, E>.andThen(
  transform: (T) -> SimpleResult<U, E>
): SimpleResult<U, E> {
  return when {
      // If the data exists, transform it
      data != null -> transform(data)
      // If it doesn't, don't do anything, just return this monad
      else -> this
  }
}

Readers that are familiar with RxJava or Flow will notice that andThen is identical to flatMap, where rather than operating on the “success” type and returning the new type in a lambda, we return a new monad instead. This allows us to chain operations that themselves return monads. This type of composition has a scary name, monad comprehension, but all it means is the ability to map and chain operations using andThen. It’s literally just being able to chain actions sequentially. And remember, the benefit of this is that we can do this over and over, handling the final type or the error whenever we decide we want to.

fun firstOperation(): SimpleResult<SomeData, Throwable> = // ...
fun secondOperation(data: SomeData): SimpleResult<SomeMoreData, Throwable> = // ...
fun thirdOperation(): SimpleResult<EvenMoreData, Throwable> = // ...

val result: SimpleResult<EvenMoreData, Throwable> = firstOperation()
  .andThen { someData -> secondOperation(someData) }
  .andThen { thirdOperation() }

What next?

Looking at this, it’s not too hard to build-your-own SimpleResult monad implementation and it’s an exercise that I think is worth doing for your own understanding and enjoyment. However, we decided to use an off-the-shelf solution to avoid any chances of making silly mistakes and so that we could move much more quickly, and so that we wouldn’t have sunk too much time into this idea if it turned out it didn’t work for us. Once we decided that this was the route we wanted to go, we spent a while evaluating the various Kotlin monad implementations out there, and we ended up settling on kotlin-result.

Why not Either?

As a small addendum, it’s worth pointing out why we didn’t use a classical Either monad for this task, as it’s probably the most commonly used Monad outside of pure functional programming and there’s plenty of open-source libraries out there implementing it. Generally, an Either is implemented like so:

data class Either<L, R>(
  val left: L? = null,
  val right: R? = null
)

The issue with this is that there’s no explicit definition of how you model success and failure. In Haskell, Data.Either users the right value as the “correct” value (“right” also means “correct”), left as failure. As far as I can tell, many languages use this convention but there’s absolutely nothing stopping you from using left as success, and this could cause a lot of confusion if switched. This also means we’d be using functions like mapLeft vs mapError, which aren’t as clear. Some libraries even offer a swap function that converts an Either<A, B> to an Either<B, A>. We wanted to avoid this.

Unwrapping kotlin-result

One of the reasons we chose this library is that out of the box, pretty much every operator you could possibly want has been implemented. Just while writing this I discovered a new one (toErrorIf) which I could have used last week - which speaks to the thoughtfulness of the author. What follows are some examples of the API, and how we’ve integrated these.

The basic API

kotlin-result uses the class Result<T, R> as the definition of a sealed class, with subclasses Ok<T> and Err<R>. These aren’t nested, which means that you don’t instantiate an Ok object with Result.Ok which reduces verbosity a bit. Check out the actual implementation here but it’s effectively this:

sealed class Result<V, E>

class Ok<T>(val value: V) : Result<V, Nothing>()
class Err<R>(val error: E) : Result<Nothing, E>()

There’s also the handy runCatching which allows us to wrap operations that might explode:

val result: Result<String, Throwable> = runCatching {
  anOperationThatCouldFail()
}

Map and MapError

Simple enough: these are your bread and butter, and allow you to convert both Ok types and Error types:

val initial: Result<String, Throwable> = // ...

val map: Result<Int, Throwable> = initial.map { it.ToInt() }

val mapError: Result<String, DomainError> = initial.mapError { it.toDomainError() }

Remember that these are the equivalent of mapLeft and mapRight, but much more explicit in their purpose.

FlatMap/andThen

You’ll use these all the time for joining one operation that returns Result with another. As we discussed earlier, the underlying implementations are the same (more accurately, flatMap calls andThen under the hood):

fun firstOperation(): Result<SomeData, Throwable> = // ...
fun secondOperation(data: SomeData): Result<SomeMoreData, Throwable> = // ...
fun thirdOperation(): Result<EvenMoreData, Throwable> = // ...

val result: Result<EvenMoreData, Throwable> = firstOperation()
  .andThen { someData -> secondOperation(someData) }
  .flatMap { thirdOperation() }

Which should you choose? That’s entirely up to you. andThen is a lot more clear and easily understandable for those who are new to functional concepts, while flatMap might be a bit more technically correct. For what it’s worth, we tend to use andThen to make our intentions super clear.

Get, GetOrElse, GetError

These are how you can unwrap the values at the end of your operation. get returns a nullable value as does getError. getOrElse is handy: it lets your define a fallback value incase your chain failed at somepoint. We use this when we fetch Strings for a screen from the server - if this operation fails, we can provide local fallbacks instead.

Ok("Hello world!").get() shouldBeEqualTo "Hello world!"

Err("This has failed").getError() shouldBeEqualTo "This has failed"

Err("This has failed").getOrElse { "Hello world!" } shouldBeEqualTo "Hello world!"

What’s neat here too is that the library exposes both the Ok and Err values via Kotlin’s component functions, which allows us to use destructuring declarations if we want to:

fun getResult() : Result<String, Throwable> = Ok("Hello world!")

val (value, error) = getResult()

Fold

This is an operator we use all the time. fold allows us to map both the error and success types differently, but they must both return the same type. Our particular usecase where this is extremely handy is in our Processor classes. We use MVI, and we’ve defined interfaces that the Processor registers to handle incoming events. Regardless of whether or not the operation is successful, we still need to return a subclass of a type:

val loadUi: FlowTransformer<Action.LoadUi, Update> =
  FlowTransformer { upstream ->
    upstream.flatMapLatest {
      val result = someUseCase()

      result.fold(
        success = { Update.UpLoaded(it) },
        failure = { Update.Error }
      )
    }
  }

As you can see we’re using Result in combination with Kotlin’s Flow classes and it’s been super productive. As we’ve invested heavily in Flow (and previously RxJava) we were already borrowing a tonne of functional programming concepts, and this extra step has come quite naturally to us.

And a whole lot more

Without going into too much detail about the exact API, there’s an awful lot more operators here covering every usecase you could think of. We’ve found this library to be very comprehensive, and it’s take a lot of nice ideas from languages such as Haskell, Rust, Elm and Scala. As a result (heh), we’ve ended up wrapping all of our network calls in runCatching and we now use Result all the way up to the presentation layer, where we finally unfold the values as you’ve seen in the above example. It’s a very important part of our stack now, and the entire team really enjoys working with it.

Pro-tip: Aliases

If you wrap API calls in runCatching (or have implemented a custom type adapter in Retrofit), you’ll find yourself defining very similar function return types all the time. Kotlin lets you define a typealias which can save you some repetitive typing.

typealias ApiResult<T> = Result<T, Throwable>
Result vs Result

One slight annoyance you’ll find is that Result clashes with the Kotlin Result class and I used to constantly find I’d imported the wrong one (you’ll know quickly because kotlin.Result only accepts on type parameter) - make sure you exclude kotlin.Result from auto-import. There’s a handy screenshot here if you’re not sure how to do that.

Rounding up

Hopefully walking through the problem we were having and explaining the solution step-by-step has helped you understand what on earth a Result Monad really is, and how simple they can make your workflow. Obviously we’ve sung the praises of kotlin-result here, but there’s plenty of other options out in the wild including the absolutely amazing Arrow project. Whilst we decided that we didn’t quite need all of many operators and classes that Arrow provides, I highly recommend reading through their documentation if this has piqued your interest - they explain core functional concepts very well: far beyond what I’ve breezed through today.

I’d also recommend that you attempt to build such an API yourself as an exercise. It’ll get you super comfortable with generics and really cement the concepts laid out here in your mind.

Lastly, thanks to Michael Bull for building kotlin-result.

Thanks for getting this far: please let me know if there’s anything you think I’ve missed or would like some more information on.

comments powered by Disqus