Railway Oriented Programming in Kotlin

I recently read Scott Wlaschin’s slide deck on Railway Oriented Programming and it got me rather excited how it closely relates to the Result type that has been around for a while in various Kotlin and Swift libraries (and has recently been added to both their core libraries). Like many others I use it regularly — yet here are railway junctions giving some theoretical foundation to the use of the Result type and clearly explaining why it is a great solution to handling errors.
I was intrigued to know how we might implement this in Kotlin while preserving the beauty of Scott’s F# examples. And so began an extended conversation with Charles Allen via code snippets on his Facebook post (who I thank for introducing me to this in first place) that eventually led to this article. [Enough introduction,] I will jump straight into a Kotlin example and try to show how it might be given the railway treatment.
Let’s suppose that we are responsible for writing a module that reads in the content of an email (e.g. from disk) and performs the send operation.
I’m going to start by introducing a happy path:
val input = read()
val email = parse(input)
send(email)
where:
fun read(): List<String> { // Get the raw data }fun parse(inputs: List<String>): Email { // Convert it to an object }fun send(email: Email) { // Perform the operation }data class Email(to: String, subject: String, body: String)
So what is an unhappy path? It is when you don’t get the perfect outcome. An example would be if the input ended unexpectedly and returned null (like you get with readLine).
val input = read()
if (input != null) {
val email = parse(input)
send(email)
}
else {
println("Error: Unexpected end of input")
}
Unhappy paths can build up quite quickly. What if the parse function can throw an exception?
val input = read()
if (input != null) {
try {
val email = parse(input)
send(email)
} catch (e: Exception) {
println("Error: Unable to parse input")
}
} else {
println("Error: Unexpected end of input")
}
Then you remember that you need to handle the validation. The email must have a valid “to” address and the subject and body must not be empty.
val input = read()
if (input != null) {
try {
val email = parse(input)
if (!email.to.contains("@")) {
println("Error: Invalid email address")
} else if (email.subject == "" || email.body == "") {
println("Error: Subject and body must not be blank")
} else {
send(email)
}
} catch (e: Exception) {
println("Error: Unable to parse input")
}
} else {
println("Error: Unexpected end of input")
}
I’m afraid to say that a lot of my code looks like this! A feature starts off simple, but as unhappy paths are added it becomes full of if/elses or try/catches (or the odd switch statement to maintain my reputation). The problem here is that the paths go deep — IntelliJ will remind you that deeply nested code is a code smell. I feel safe with the “flat” code of the happy path, but not with the “depthy” code of this last example. Why is it that I don’t like the depthy code?
- The happy path is no longer obvious. The logic of the happy path is input-parse-validate-send, but it is lost in ifs and trys.
- There are lots of unhappy paths and they are all in one function. The more branching, the more likely we are to get into unexpected states. I suddenly need unit tests to raise my confidence in this code.
- It is difficult to see all the possible unhappy outcomes. (Can you spot them? They are: a) unexpected end of input, b) parse failure, c) invalid email address, d) missing subject or body)
- The unhappy outcomes are not always located in the same place as the detection of the unhappy path. (The unexpected end of input outcome is at the bottom, but the check for it is at the top.)
Railway Oriented Programming offers a functional approach to handling happy and unhappy paths (or successful states and error states). Let’s see how it can be implemented in Kotlin.
The main principle in Railway Oriented Programming is that every function can returns either success or failure (in the same way that a railway junction has two tracks out of it). In our example, bothparse
and send
are functions that have a happy outcome (success) or an unhappy outcome. The validation steps could be considered the same. Let’s write a separate function for the validation of the email address:
fun validAddress(email: Email): Result =
if (email.to.contains("@"))
Success(email)
else
Failure("Invalid email address")
Both functions take an Email
object as a parameter and return a Result
object containing either Success
or Failure
. In the case of Success
the object will also contain the Email
object. In the case of Failure
the object will also contain a String
error message.
The Result
is a sealed class in Kotlin (read: enum on steroids) whose basic implementation might look something like this:
sealed class Result
data class Success(val value: Email): Result()
data class Failure(val errorMessage: String): Result()
But later we will want to use Result
for any type of success, so let’s replace Email
with a generic type.
sealed class Result<T>
data class Success<T>(val value: T): Result<T>()
data class Failure<T>(val errorMessage: String): Result<T>()
Then update our validAddress
function to use the generic Result
and introduce the second validation function for checking the email is not blank.
fun validAddress(email: Email): Result<Email> =
if (email.to.contains("@"))
Success(email)
else
Failure("Invalid email address")fun notBlank(email: Email): Result<Email> =
if (email.subject != "" && email.body != "")
Success(email)
else
Failure("Subject and body must not be blank")
These two functions might take up more lines of code than the validation code in our “depthy” example, but we have gained a piece of documentation in the function name and the logic of their unhappy outcomes is plain to see. We could use them individually:
val result = validAddress(email)
when (result) {
is Success -> println("Ok")
is Failure -> println("Error: ${result.errorMessage}"}
}
Or the two functions could be composed:
val result = validAddress(email).then(::notBlank)
when (result) {
is Success -> println("All validation checks passed")
is Failure -> println("Error: ${result.errorMessage}"}
}
Or the composition could be defined as a new function that performs all the validations:
fun validate(email: Email) = validAddress(email).then(::notBlank)val result = validate(email)
when (result) {
is Success -> println("All validation checks passed")
is Failure -> println("Error: ${result.errorMessage}"}
}
What is then
? It is a function that we define on Result
that applies the given function only on Success
results (otherwise it returns the existing Failure
value):
infix fun <T,U> Result<T>.then(f: (T) -> Result<U>) =
when (this) {
is Success -> f(this.value)
is Failure -> Failure(this.errorMessage)
}
If then
is defined as an infix function then it becomes more naturally readable (especially for the functional enthusiasts out there — Warwick Alumni of CS123, you know who you are):
fun validate(email: Email) = validAddress(email) then ::notBlank
(The double colon prefix is a function reference which is needed in Kotlin to pass a top-level function around.)
Back to the railway analogy… Each function that returns a Result
is the logic for a railway junction that evaluates to success or failure. They are composed to form sequences of junctions where the happy path is one where all the functions get executed (Success/green) and any failure causes the train to redirect to the unhappy path (Failure/red).

Suppose our railway involves three main junctions defined above as parse
, validate
, and send
. The validate
function was given earlier as the composition of detecting invalid address and detecting blank subject/body. The other two functions are defined as follows:
fun parse(inputs: List<String>): Result<Email> { ... }fun send(email: Email): Result<Unit> { ... }
Then our earlier example can be implemented as the following:
val input = read()
parse(input) then ::validate then ::send
I don’t think there is anyway to make the happy path of our module clearer than this! And we have encapsulated our unhappy outcomes in each individual function.
How do we deal with the unhappy paths? At the end of the railway, any failure is passed to an error handler. I introduced another infix function (named otherwise
) specifically for handling any failures at end of the chain:
infix fun <T> Result<T>.otherwise(f: (String) -> Unit) =
if (this is Failure) f(this.errorMessage) else Unit
Thus, we can now handle any failure with:
parse(input) then ::validate then ::send otherwise ::errorfun error(message: String) = println("Error: ${message}")
Finally (and this is optional) we could pipe the input into the railway by just passing in the first Success
object:
Success(input) then ::parse then ::validate then ::send otherwise ::error
I prefer not seeing that Success
at the beginning, so let’s introduce a pipe function named to
that does the work for us:
infix fun <T,U> T.to(f: (T) -> Result<U>) = Success(this).then(f)
I hope you will agree that our final implementation of the email sending module looks pretty concise:
input to ::parse then ::validate then ::send otherwise ::error
The complete example code is available as a Github Gist. 👩🏻💻
That’s how I implemented Railway Oriented Programming in Kotlin — there may be better ways in which case I would love to hear from you in the comments.
It was a useful exercise to implement Result myself, but Kotlin now has a Result
type baked in, ready for you to use (as of 1.3), and it’s a bit more powerful than mine! For a start, it wisely replaces the String
error message with an exception Throwable
. It has a map
function instead of then
and an onFailure
function instead of otherwise
. I used infix functions for natural readability but I suspect hardcore Kotlin-ers will prefer how it looks with Kotlin’s built-in Result
type:
parse(input).map(::validate).map(::send).onFailure(::error)
The only reason that you might not want to use the built-in Result type is that the compiler doesn’t allow it to be the return type of a function by default (which is exactly how you want need to use it!) — however, it can be enabled by adding a compiler flag.
Enjoy building your own railways in Kotlin and keep preserving the beauty of happy paths! 🚂