Refactoring if-else: Either and FlatMap

Railway Oriented Programming with Kotlin and Arrow

Hari Krishnan
polarizertech
4 min readJun 30, 2020

--

Attribution: Dzp / CC BY-SA (https://creativecommons.org/licenses/by-sa/3.0)

This post expands on the concepts I had covered in my earlier post about “Refactoring to Railway Oriented Programming”. Other than being a great approach to handling errors, in my experience, Railway Oriented Programming also serves as a great use case for explaining the need for Monads to first timers. While understanding Monads is hard enough, trying to explain it to others is a lot harder. However, this post is not a tutorial on Monads, instead it just explains the concepts in the context of ROP.

We will be leveraging Arrow-kt library and specifically the Either monad, FlatMap and Fold operators to refactor below snippet.

Step-1: The “Either” Monad

All we are going to do in this step is to extract the code in each “if ” condition to methods. More importantly these methods should return an “Either.Right” or “Either.Left”. Right is returned on success and Left is returned on Error.

fun matchUrl(anotherRequest: HttpRequest): Either<String, HttpRequest> {
if (this.url != anotherRequest.url)
return Either.Left("URL did not match. ${this.url} not equal to ${anotherRequest.url}")
return Either.Right(anotherRequest)
}

fun matchMethod(anotherRequest: HttpRequest): Either<String, HttpRequest> {
if (this.method != anotherRequest.method)
return Either.Left("Method did not match. ${this.method} not equal to ${anotherRequest.method}")
return Either.Right(anotherRequest)
}

fun matchBody(anotherRequest: HttpRequest): Either<String, HttpRequest> {
if (this.body != anotherRequest.body)
return Either.Left("Body did not match. ${this.method} not equal to ${anotherRequest.method}")
return Either.Right(anotherRequest)
}

Step-2: The “FlatMap” Operator

Either is a Monad, or in over simplified terms a wrapper around raw values that allows a bind transformation. Let us understand this in detail.

  1. Wrapper around raw values. Example: In the above step, we are wrapping the error message String in “Either.Left” and the HttpRequest in “Either.Right” respectively.
  2. Bind transformation. The bind operator that we will be leveraging on Either monad is FlatMap. FlatMap takes a function as an argument and applies it on the wrapped value of the Either Monad and returns a Monad. The function that FlatMap takes as the argument, should accept the unwrapped value of the the Either Monad as the argument and return another Monad. Example: We have written such functions in Step 1.

Why all these rules? Because now we will be able to sequence method calls as shown below.

matchUrl(request).flatMap { matchBody(it) }.flatMap { match... }...

The matchUrl method returns an Either monad. The flatMap applies matchBody to the unwrapped value (“it”) if the Either monad. Now matchBody returns and Either monad and flatMap applies the next function and so on.

This all sounds good when the matchUrl returns “Either.Right” and then matchBody also returns an “Either.Right” and so on because the wrapped value in “Either.Right” is the HttpRequest which each of the subsequent functions take as argument. However what if matchUrl returns “Eight.Left”? The wrapped value is a String. What do we do?

Either Monad is “Right-Biased”

What is right bias? I earlier mentioned that the bind operator applies the function on the wrapped value. However, Either can have two possible values (Classified as Sum Type in ADT). Which value should we apply the function on? This is why flatMap in Either only applies the function on the Right.

What about the errors? We still need to stop the execution of other functions when a function returns error. On Left, the flatMap in Either short circuits the execution and does not continue. Example: When matchUrl returns Left, matchMethod and matchBody are not invoked.

I now need to be able to extract the error message when there is a problem and I want the HttpRequest when it is successfully matched.

This is where “Fold” operator is necessary. “Either.Fold” takes two functions, ifLeft to apply when the overall result is “Either.Left”, ifRight to apply when the overall result is “Either.Right”. It is also able to access the wrapped value, error String when “Either.Left” and HttpRequest when “Either.Right”.

With flatMap and fold we can now solve this refactoring.

private fun matchAnotherRequest(anotherRequest: HttpRequest) =
matchUrl(anotherRequest).flatMap {
matchMethod(it)
}.flatMap {
matchBody(it)
}.fold({ Failure(it) }, { Success(it) })

Step-3: Extracting “then” and “handleError”

This step is optional. To improve readability I have formatted above code to keep all the method names on the left. All the machinery has been moved to the right.

matchUrl(anotherRequest)    .flatMap {
matchMethod(it) }.flatMap {
matchBody(it) }.fold(
{ Failure(it) }, { Success(it) }
)

This code on the right can be moved to its own place. The flatmap related code can move to “then” and fold related code can move to “handleError”.

Putting it all together.

--

--

Hari Krishnan
polarizertech

Full Stack Developer, Architecture Consultant, Lean/XP Coach and Trainer. https://about.me/harikrishnan83