Comprehension-like syntax in Kotlin

When working with monads it is very common to face situations where multiple nested map and flatMap calls are needed. As an example, let’s take a hypothetical snippet for a Mars Rover kata solution (with some changes just to point out the problem):

val io = printIntroductionText()
        .flatMap { retrieveWorldSizeKm() }
        .map { worldSizeKm -> convertKmToMiles(worldSizeKm) }
        .flatMap { worldSizeMiles ->
            readInitialPosition(worldSizeMiles)
                    .flatMap { pos ->
                        readInitialDirection()
                                .flatMap { dir ->
                                    initState(worldSizeMiles, pos, dir)
                                }
                    }
        }

As you can see given that the initState function requires all the retrieved information from the previous functions, the nesting is needed.

Some other languages, like Scala, provide an alternative syntax for nested flatMap calls. In Scala, you would write:

val io = for {
    _              <- printIntroductionText()
    worldSizeKm    <- retrieveWorldSizeKm()
    worldSizeMiles  = convertKmToMiles(worldSizeKm)
    pos            <- readInitialPosition(worldSizeMiles)
    dir            <- readInitialDirection()
    s              <- initState(worldSizeMiles, pos, dir)
} yield s

This syntax is much more readable and there is much less noise. Unfortunately, Kotlin doesn’t provide any tools like this. While the Arrow team is working on a compiler plugin to provide in Kotlin something similar to the Scala solution, I was wondering if I could find out a way to make the code from the initial snippet at least a bit more readable using plain Kotlin.

Since the first KIO release, I tried to mitigate this problem by offering a particular version of map and flatMap called mapT and flatMapT; these T functions get the result from the argument function and put it in a tuple with the provided input parameter. Using these functions, the initial code would become:

val io = printIntroductionText()
    .flatMap { retrieveWorldSizeKm() }
    .map { worldSizeKm -> convertKmToMiles(worldSizeKm) }
    .flatMapT { worldSizeMiles -> readInitialPosition(worldSizeMiles) }
    .flatMapT { (_, _) -> readInitialDirection() }
    .flatMap { (size, pos, dir) -> initState(size, pos, dir) }

This solution already makes the code much more readable: the nesting has gone. But as the steps number increase, you have to repeat the tuple destructuration; since the tuple size becomes bigger at every step, also the noise generated by this solution increases considerably. Also, even with this solution, you have more noise in the code than in the Scala solution.

So, lately, I started experimenting again to find out some other alternative. After some work, I found out a different solution, that is the following:

val io = 
    printIntroductionText()                 +
    retrieveWorldSizeKm()                   to  { worldSizeKm ->
    convertKmToMiles(worldSizeKm)           set { worldSizeMiles ->
    readInitialPosition(worldSizeMiles)     to  { pos ->
    readInitialDirection()                  to  { dir ->
    initState(worldSizeMiles, pos, dir)
}}}}

I’ve introduced three new elements in the syntax:

  • the + operator is used to sequence effects when the output of the first operand isn’t needed (both if it is of Unit type or not);
  • the to infix function, that extracts the output of the same-row function call and introduces a new variable in the comprehension context with that value;
  • the set infix function, that is equivalent to the to function but must be used if the same-row function call doesn’t return an effect instance (basically the difference is the same of map and flatMap chaining).

In my opinion, the resulting code is much more readable than the code in the initial snippet: the calls sequence is very visible on the left “column” and, to me, this is the most important part of the code; in fact, looking to a piece of code, you should be able to quickly understand what is its purpose and which functions interact with it. Also, there is very little noise if compared with the initial solution or with the flatMapT one.

I’m going to release this new syntax it the upcoming KIO 0.5 release, in addition to the already available standard map/flatMap and tuple-based mapT/flatMapT solutions. I would be very happy to know what do you think about this solution and if you have any suggestion to improve it. As always, you can reach me on Twitter.


P.S.: if you like to auto-format your code with IntelliJ IDEA like me, you can use this feature in order to keep the right formatting for your comprehension code blocks: Exclude code fragments from reformatting in the editor