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 byrunCatching
and its receiver variant - Extracting the value from a single
Result
-getOrThrow
,getOrElse
,getOrDefault
,getOrNull
,exceptionOrNull
, or more generallyfold
- Transforming a single
Result
into a differentResult
- done bymap/recover
and theircatching
variants, or more generallyfold
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 A
, B
etc. return instances of Result
— we're adults now, and exceptions are for children. If they didn't return Result
s, 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' Result
s, 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:
- the subprocesses were dependent on the result of any number of their predecessors
- we were composing “asymmetrical” functions of the form
T -> Result<R>
, as opposed toT -> 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:
- always returning values, therefore preserving standard control flow
- separating the happy path from error handling
- 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 "Promise
s!" but you couldn't quite put your finger on it, that is also not a coincidence — Promise
s 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 Promise
s 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.