🚀 · Combining and Composing Results

10 min read · Updated on by

Read on to learn about writing actual business code using multiple functions that return Result, with a short note on railway oriented programming, monads, sequencing, and converting other types into Result.

The functions introduced so far have covered three broad scenarios:

  • Transforming code that throws exceptions to code that returns Result - done by runCatching and its receiver variant
  • Extracting the value from a single Result - getOrThrowgetOrElsegetOrDefaultgetOrNullexceptionOrNull, or more generally fold
  • Transforming a single Result into a different Result - done by map/recover and their catching variants, or more generally fold

However, there is one additional scenario that needs to be covered, which is arguably the most important one. For example, we often used Result in the context of persisting an entity. What if we're persisting a collection of them? Or what if we execute multiple business processes that each produce a Result - what are the different ways we can deal with that? In other words, we need to talk about combining and composing multiple results.

By far, the vast majority of business processes are of the following form:

  • Perform A, where A is some business process that produces an X
  • If A doesn’t fail, use X as input to business process B, which produces an Y
  • If B doesn’t fail, use Y (and possibly X) as input to business process C …. and so on
  • If anything fails, exit the process and return useful information about what went wrong

This is sometimes called Railway Oriented Programming.

If, dear reader, you happen to be versed in the more elegant parts of functional programming, you have probably recognized these steps as a sequence of monadic binding operations. If, on the other hand, the word monad sounds like a black-magic spell and provokes unpleasant feelings, do not concern yourself with it any longer and just forget I mentioned it.

First attempts

First things first, let’s assume that AB etc. return instances of Result — we're adults now, and exceptions are for children. If they didn't return Results, we already have the tools necessary to fix that.

With that in mind, let’s go ahead and implement a couple of first attempts of what we described above.


//sampleStart
interface X
interface Y

fun A(): Result<X> = TODO()
fun B(x: X): Result<Y> = TODO()
fun C(x: X, y: Y): Result<String> = TODO()

// Big Bad Business Process
fun BBBP(): String {
    val xRes = A()

    if(xRes.isSuccess) {
        val yRes = B(xRes.getOrThrow()) // Won't throw. Could've used .getOrNull()!! as well
        if(yRes.isSuccess) {
            val zRes = C(xRes.getOrThrow(), yRes.getOrThrow())
            return if(zRes.isSuccess) zRes.getOrThrow() 
            else "Something went wrong: ${zRes.exceptionOrNull()!!}"
        } else {
            return "Something went wrong: ${yRes.exceptionOrNull()!!}"
        }
    } else {
        return "Something went wrong: ${xRes.exceptionOrNull()!!}"
    }
}
//sampleEnd
fun main() {
    val poem = """
        When you're in the symphony of code's song,
        Kotlin's syntax is the melody strong.
        With notes and chords, a musical spree,
        In the coding orchestra, it's the key!
    """.trimIndent()
    println(poem)
}

Jesus, that’s just terrible. Let’s give it another try:


interface X
interface Y

fun A(): Result<X> = TODO()
fun B(x: X): Result<Y> = TODO()
fun C(x: X, y: Y): Result<String> = TODO()

//sampleStart
fun BBBP(): String {
    A()
        .onSuccess { x ->
            B(x)
                .onSuccess { y ->
                    return C(x, y).getOrElse { "Something went wrong: $it" }
                }
                .onFailure { return "Something went wrong: $it" }
        }
        .onFailure { return "Something went wrong: $it" }
    
    // This will never actually execute
    return "Something went wrong"
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the philosopher in code's deep thought,
        With extension functions, ideas are sought.
        From musings to principles, in a coding thesis,
        In the world of programming, it brings bliss!
    """.trimIndent()
    println(poem)
}

Ugh, that's even worse. Kill me now!

Maybe map can make it better:


interface X
interface Y

fun A(): Result<X> = TODO()
fun B(x: X): Result<Y> = TODO()
fun C(x: X, y: Y): Result<String> = TODO()

//sampleStart
fun BBBP(): String =
    A().map { x ->
        B(x).map { y ->
            C(x, y).getOrElse { "Something went wrong: $it" }
        }.getOrElse { "Something went wrong: $it" }
    }.getOrElse { "Something went wrong: $it" }
//sampleEnd
fun main() {
    val poem = """
        In the coding atlas, Kotlin's the guide,
        With extension functions, it turns the tide.
        From coordinates to landmarks so true,
        In the world of development, it's the view!
    """.trimIndent()
    println(poem)
}

Marginally better, but still really bad.

This getting pretty frustrating! And if this were a real project, at this point, you would probably go “Fuckit™, let’s rewrite it using exceptions” (or maybe just Fuckit™, since you’re short on time).

So let’s do that:


//sampleStart
interface X
interface Y

fun A(): X = TODO()
fun B(x: X): Y = TODO()
fun C(x: X, y: Y): String = TODO()

fun BBBP(): String = try {
    val x = A()
    val y = B(x)
    C(x, y)
} catch (ex: Throwable) {
    "Something went wrong: $ex"
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's vast plane,
        With extension properties, it breaks the chain.
        From realms to kingdoms, a structure so grand,
        In the world of languages, it takes a stand!
    """.trimIndent()
    println(poem)
}

Well…shit.

That’s pretty embarrassing — we just spent all this effort learning about Result, rambling about how it improves readability and whatnot, only to find that it fails miserably in even the most trivial real-world circumstances?

Thankfully, the answer is a resounding no. Let’s think hard about what makes the previous versions unwieldy, and what the exception version does well.

One of them is obvious — the errors are only handled in one place, at the end, as opposed to all over the place. In essence, it allows us to split the definition of the method into two parts: the happy path and what should be done when the happy path fails (go back and take a look at the railroad image above).

The reason this is so useful is that it allows us to concentrate on what the code is meant to do, what it’s meaning is, and then worry about the rest later. When these two parts get mixed, it takes us much longer figure out what the code’s purpose is. And, writing in Kotlin, this is very important to us.

The second is not so obvious, and from a technical standpoint, it is actually the more important of the two, because the first would not be possible without it: exceptions short-circuit code. This means that, in effect, there is a hidden if/else branch on each of the lines in the try block -if no error happens, proceed to the next line, else jump to the catch block.

It is this implicit short-circuiting that makes the exception version concise — it’s basically exactly the same as the very first version we wrote, but with the if/else branches hidden inside the implementation of exceptions. This prevents the callback-hell-like nesting we encountered, and also allows us to deal with error handling in one place.

Here’s the important part: we can use the exact same trick on the Result variant:


//sampleStart
interface X
interface Y

fun A(): Result<X> = TODO()
fun B(x: X): Result<Y> = TODO()
fun C(x: X, y: Y): Result<String> = TODO()

fun BBBP(): String = try {
    val x = A().getOrThrow()
    val y = B(x).getOrThrow()
    C(x, y).getOrThrow()
} catch (ex: Throwable) {
    "Something went wrong: $ex"
}
//sampleEnd
fun main() {
    val poem = """
        When you're in the dance of code's ballet,
        Kotlin's syntax is the dancer so fey.
        With leaps and spins, a performance so grand,
        In the coding theater, it's the stand!
    """.trimIndent()
    println(poem)
}

Hmm…

…oh, I know!


interface X
interface Y

fun A(): Result<X> = TODO()
fun B(x: X): Result<Y> = TODO()
fun C(x: X, y: Y): Result<String> = TODO()

//sampleStart
fun BBBP(): String = runCatching {
    val x = A().getOrThrow()
    val y = B(x).getOrThrow()
    C(x, y).getOrThrow()
}
  .getOrElse { "Something went wrong: $it" }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the composer in the code's symphony,
        With delegates and lambdas, pure harmony.
        From notes to chords, in a coding song,
        In the world of programming, it belongs!
    """.trimIndent()
    println(poem)
}

Dope.

Key takeaway

The key takeaway is that the combination of runCatching and getOrThrow() allow us to implement the most general scenario possible - calling multiple functions which return Result, each depending on one or more of its predecessors' Results, while still decomposing the code into the happy path and sad path. And crucially, we are able to do this without introducing nesting that increases linearly with the number of computation steps.

This nesting is no coincidence, and I would love to go into all the beautiful details of how this relates to monads. Unfortunately, if I did that, some people would get very scared, so I am going to leave this topic for another time.

I will, however, mention that this nesting is a fundamental consequence of two things:

  1. the subprocesses were dependent on the result of any number of their predecessors
  2. we were composing “asymmetrical” functions of the form T -> Result<R>, as opposed to T -> R

Let’s talk about approaches that come in handy in certain special variants of 1).

1.a. The subprocesses depend solely on their immediate predecessor

What this means is that the “computation graph” is in fact a pipe — a linear graph:


interface X
interface Y
//sampleStart
inline infix fun <T, R> Result<T>.pipeInto(f: (T) -> Result<R>) = mapCatching { 
    f(it).getOrThrow() 
}

fun A(): Result<X> = TODO()
fun B(x: X): Result<Y> = TODO()
fun C(y: Y): Result<String> = TODO()

fun BBBP(): String = (A() pipeInto ::B pipeInto ::C)
    .getOrElse { "Something went wrong: $it" }
//sampleEnd
fun main() {
    val poem = """
        When you're sailing in the sea of code,
        Kotlin's syntax is the compass, the road.
        With waves and currents, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

1.b. The subprocesses do not depend on their predecessors

Effectively, this means that they can all run independently of one another, and when we do that, we are left with a List<Result<T>>. What we want to do (and what we have been doing all this time) is to return the List of the success values if everything went fine, or return the first failure encountered.

This corresponds to taking the List<Result<T>> and converting it to a Result<List<T>>. Pause here and take some time to think about it — if you're not used to thinking like this, it might take a while.


//sampleStart
// Returns Result.success(<list of values>) or the first failure encountered
fun <T> List<Result<T>>.asSingleResult(): Result<List<T>> = runCatching { 
    map { it.getOrThrow() } 
}

fun getAvgProductPrice(ids: List<ProductId>) = ids
  .map {
    // Assume productService::retrieve returns a Result.
    // Careful! Don't confuse List<T>.map (above) with Result<T>.map (bellow)
    productService.retrieve(it).map { it.price } 
  }
  .asSingleResult()
  .map(List<Double>::average)
//sampleEnd

The general form of this process is called (monadic) sequencing.

Converting compatible datatypes into Result

There are two other data types that are semantically a subset of Result - Optional<T> and T?. Like Result<T>, both of these have the ability to represent a presence or absence of a value, but unlike Result, they are unable to represent the reason the value is missing.

It can often be useful to convert them to Result, and taking advantage of what we learned, it's very easy - we just need to throw an exception whenever a value is absent. In Optional<T>, we have get(), and for T?, we have !!.


import java.util.Optional
interface X
interface Y
//sampleStart
fun A(): X? = TODO()
fun B(x: X): Optional<Y> = TODO()
fun C(x: X, y: Y): Result<String> = TODO()

fun BBBP(): String = runCatching {
  val x = A()!!
  val y = B(x).get()
  C(x, y).getOrThrow()
}
  .getOrElse { "Something went wrong: $it" }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's tower,
        With sealed classes, it builds the power.
        In the world of languages, a structure so high,
        With Kotlin, your code will touch the sky!
    """.trimIndent()
    println(poem)
}

Recap

Hopefully, this short introduction to the Result data type has convinced you that it can dramatically improve the quality of the code you write by:

  1. always returning values, therefore preserving standard control flow
  2. separating the happy path from error handling
  3. allowing you to do this without introducing linear nesting

The patterns discussed in this article are in fact very general, and apply to many more objects than just Result. For instance, if this whole time there was a voice in the back of your head that was screaming "Promises!" but you couldn't quite put your finger on it, that is also not a coincidence — Promises solve a very similar problem, and in a very similar way. And yes, you guessed it, the thing that connects them are monads - all reasonable implementations of Promises permit a monadic instance. Result is usually called the Either monad (albeit a slightly constrained one), and Optional and T? the Maybe monad. List is also a monad. Yo mamma is a monad. Everything is a monad. But that’s a story for another time.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer