avatarGabriel Shanahan

Summary

This article discusses programming with the Result type in Kotlin, emphasizing combining and composing multiple Result-returning functions, converting other types to Result, and the importance of handling errors effectively while avoiding deeply nested code structures.

Abstract

The article, part of the Kotlin Primer series, focuses on the practical use of the Result type in Kotlin for implementing robust business logic. It covers scenarios where multiple functions return Result objects, illustrating the importance of handling these in a way that maintains code readability and avoids the pitfalls of traditional error handling with exceptions. The author demonstrates various approaches to chaining Result-returning functions, including railway-oriented programming and monadic operations, to manage dependencies between subprocesses. The article also touches on converting Optional<T> and T? types to Result, highlighting the benefits of monadic sequencing to streamline error handling and improve code quality. By leveraging Result, developers can separate the "happy path" of their code from error handling, leading to more maintainable and predictable software.

Opinions

  • The author believes that the traditional use of exceptions for error handling can lead to unwieldy and difficult-to-maintain code, particularly in complex business logic.
  • Railway-oriented programming and monads are presented as superior alternatives to exceptions for handling errors and composing functions that return Result.
  • The article suggests that converting Optional<T> and T? to Result can enhance the semantic meaning of missing values by allowing for explicit failure representation.
  • The author expresses that managing errors in one place rather than scattered throughout the codebase is crucial for code clarity and maintainability.
  • The use of Result is advocated for its ability to preserve standard control flow, separate the happy path from error handling, and prevent linear nesting of code.
  • The article implies that understanding monads can be beneficial, even though the concept may intimidate some developers.

Programming with Result: Combining and Composing Results

Writing actual business code using multiple functions that return Result, with a short note on railway oriented programming, monads, and sequencing. Converting other types into Result.

— — — — — — — — — — — — — — —

THE CURRENT VERSION OF THIS ARTICLE IS PUBLISHED HERE.

— — — — — — — — — — — — — — —

Tags: #FUNDAMENTAL CONCEPT

This article is part of the Kotlin Primer, an opinionated guide to the Kotlin language, which is indented to help facilitate Kotlin adoption inside Java-centric organizations. It was originally written as an organizational learning resource for Etnetera a.s. and I would like to express my sincere gratitude for their support.

It is recommended to read the Introduction before moving on. Check out the Table of Contents for all articles.

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 - getOrThrow, getOrElse, getOrDefault, getOrNull, exceptionOrNull, 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
Railway Oriented Programming

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 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 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.

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

Ugh, kill me now!

Maybe map can make it better:

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 probably short on time).

So let’s do that:

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:

Hmm…

…oh, I know!

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:

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.

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 !!.

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.

Go back to Programming with Result: kotlin.Result, jump to the Table of Contents, or continue to Programming with Result: Considerations.

Java
Kotlin
Programming
Functional Programming
Exception Handling
Recommended from ReadMedium