Happy Railway
This post is on the tail of Railway Oriented Programming in Kotlin by Antony Harfield. So you need to read it first and continue here. As it’s obvious I really liked it and tried it out. It needs every process have a result like The generic result
sealed class Result<T>
data class Success<T>(val value: T): Result<T>()
data class Failure<T>(val errorMessage: String): Result<T>()
Then we could chain the processes and create our railway like
input to ::parse then ::validate then ::send otherwise ::error
It’s so satisfying and shows what we want to achieve so clearly until we notice the Result
is not native to every processes. It’s a general wrapper. For instance the Result
of ::validate
will be more informative if it would be like
sealed class ValidationResult
data class ValidationSuccess(val email: Email): ValidationResult()
data class InvalidEmailAddress(val errorMessage: String): ValidationResult()
data class EmptySubject(val errorMessage: String): ValidationResult()
data class EmptyBody(val errorMessage: String): ValidationResult()
By informative I mean the caller of this method will easier/cleaner branch its code with when
expression/statement. The same can be applied to send method. The more native Result
for ::send
can be like
sealed class SendResult
data class SendSuccess(val email: Email): SendResult()
data class IOFailure(val error: IOException): SendResult()
data class Timeout(val error: TimeoutException): SendResult()
data class UnAuthorized(val error: UnAuthorizedException): SendResult()
But in these cases we will lose our clean code(Not “The Clean Code”) to show what is actually going on. To solve it you may need to go and read Railway Oriented Programming in Kotlin again, where by using Railway Oriented Programming we actually want to clarify what the happy path of the proccess is. This implies chaining the Success
es on the above results can bring us an almost similar clean code as before. But how?!
If we try to write the happy path in plain English it would be like
parse input
- if failed return proper error messagevalidate
- if there is an invalid email address return proper error message
- if subject is empty return proper error message
- if body is empty return proper error messagesend email
- if there is a IO problem return proper error message
- if there is a Timeout return proper error message
- if unauthorized, return proper error message
Do you read it easily? Can you find the happy path at the first glance? It’s because of separation of the happy path from the failures by indenting the failure lines. That’s how our brain trained to read fast. If we could manage to write our code like this then the happy path will be clear for our brain at the first sight. We will call it a concise code. The first take away from the concise code above is that the successes define the happy path. The second one is that the success result is one and only one but the failures can be many. Also we just returned the proper error messages on all branches, but potentially you can do anything in those cases. The practical usecase can be replacing the failure with the default value where we will come back to it later.
Proposal
In the concise code above I see some kind of when
expression, but it distinguishes the success result from the failures. It’s not an ordinary when
expression in Kotlin. But Kotlin is a powerful language where supports DSLs. Maybe we can create a DSL to do this job for us.
For instance let’s try to create a DSL for ValidationResult
. This DSL must returns the happy result and have something like when
statement to branch the code for different failures. Something like
validate() elseIf {
InvalidEmailAddress { errorMessage: String ->
/* handle failure */
}
EmptySubject { errorMessage: String -> /* handle failure */ }
EmptyBody { errorMessage: String -> /* handle failure */ }
}
Note this DSL must use inline
functions to be able to break the flow and do something like below with return
doWork() : Result<Email> {
...
validate() elseIf {
InvalidEmailAddress { errorMessage: String ->
return Failure(errorMessage)
}
EmptySubject { errorMessage: String -> /* handle failure */ }
EmptyBody { errorMessage: String -> /* handle failure */ }
}
...
To create this DSL we need a builder
public class ValidationResultElseIfBuilder(
public val parent: ValidationResult
) {
public lateinit var result: Success
public inline fun InvalidEmailAddress(block: (errorMessage: String) -> Success): Unit {
if(parent is InvalidEmailAddress) {
result = block(parent.errorMessage)
}
}
public inline fun EmptySubject(block: (errorMessage: String) -> Success): Unit {
if(parent is EmptySubject) {
result = block(parent.errorMessage)
}
}
public inline fun EmptyBody(block: (errorMessage: String) -> Success): Unit {
if(parent is EmptyBody) {
result = block(parent.errorMessage)
}
}
Also we need to define the elseIf
extension method like this
public inline infix fun ValidationResult.elseIf(build: ValidationResultElseIfBuilder.() -> Unit):
Success {
if (this is Success) {
return this
} else {
val builder = ValidationResultElseIfBuilder(this)
builder.build()
return builder.result
}
}
It’s a proof of concept DSL. Notice the result
is lateinit
which makes this DSL exhaustive on runtime, but unfortunately Kotlin DSL doesn’t have mandatory annotation or something like that, so we cannot force it on compile time :|
However, we can do the same for SendResult
so will be able to call the send method like this
send() elseIf {
IOFailure { error: IOException -> /* handle failure */ }
Timeout { error: TimeoutException -> /* handle failure */ }
UnAuthorized { error: UnAuthorizedException ->
/* handle failure */
}
}
Then the doWork
method will look like
fun doWork(): Result<Unit> {
return input() into
::parse elseIf
{
return Failure("Cannot parse email")
} into
::validate elseIf
{
InvalidEmailAddress { message -> return Failure(message) }
EmptySubject { message -> return Failure(message) }
EmptyBody { message -> return Failure(message) }
} into
::send elseIf
{
IOFailure { error -> return Failure(error.message ?: "") }
Timeout { error -> return Failure(error.message ?: "") }
UnAuthorized { error -> return Failure(error.message ?: "") }
} into
{ Success(Unit) }
}
fun parse(input: String): Result<Email> = ...
fun validate(input: Success<Email>): ValidationResult = ...
fun send(input: ValidateSuccess): SendResult = ...
where into
is
inline infix fun <T, R> T.into(block: (T) -> R): R {
return block(this)
}
which its definition is the same as let
with infix
modifier. It’s the best implementation that I came with where it arranges the failures to have an indent respect to the happy path, so probably it’s more readable to our brain.
Also did you notice the input of validate
and send
methods. They force the caller to finish the previous step successfully, which is fantastic. What do you think?
But who would pay for the boilertrap code the DSL needs! To solve this last problem, you can checkout Happy code generator. Also you can find above example with more details in the tests of this repository here.
More Complex Scenario
Let’s have a concise code like
parse input
- if failed return proper error messagevalidate
- if there is an invalid email address return proper error message
- if subject is empty fix it by replacing the default subject
- if body is empty return proper error messagesend email
- if there is a IO problem return proper error message
- if there is a Timeout return proper error message
- if unauthorized, try to authorize then send it again.
As you can find out, there are two changes respect to the previous concise code. First in case of the empty subject we can fix the email, we will replace it with default subject, and the second one is the unauthorized failure can be fixed by trying to authorize and resend the email. In the end, we can use the above DSL and sealed classes before with a little bit of changes, but the concept is the same. So the code will be like
fun doWork(): Result<Unit> {
return input() into
::parse elseIf
{
return Failure("Cannot parse email")
} into
::validate elseIf
{
InvalidEmailAddress { message -> return Failure(message) }
EmptySubject(::fixEmptySubject)
EmptyBody { message -> return Failure(message) }
} into
::send elseIf
{
IOFailure { error -> return Failure(error.message ?: "") }
Timeout { error -> return Failure(error.message ?: "") }
UnAuthorized { validatedEmail, _ ->
validatedEmail into
::authorizeAndSend elseIf
{
return Failure(it)
} into
{ SendSuccess(validatedEmail.email) }
}
} into
{ Success(Unit) }
}
Can you read it at the first glance? Again the failures are indented respect to happy path. By the way, you can find the complete code of this example in the previous repository but with different commit here.
Hope you enjoy coding more with this DSL. Don’t forget to upvote this post and please leave some feedbacks on comments. Thank you for your time!
Original Post
This article originally posted on ProAndroidDev.