Functional Programming with Kotlin and Arrow – Generate Typeclasses With Arrow

In this Kotlin tutorial, you’ll take the functional programming concepts learned in previous tutorials and apply them with the use of the Arrow framework. By Massimo Carli.

Leave a rating/review
Download materials
Save for later
Share

In the previous tutorial Functional Programming with Kotlin and Arrow Part 3: More about Typeclasses, you created a simple app using typeclasses to fetch information from a remote endpoint. You used the Result<E,A> datatype along with the functions for making it a Bifunctor, an Applicative and, finally, a Monad. In future tutorials, you’ll have the opportunity to further your understanding of the theory behind these important FP concepts. For this tutorial, you’ll implement the features of Result<E,A> that we created in previous tutorial with the Arrow framework.

Working through this tutorial, you’ll:

  • Learn how to define a data type with Arrow which enables code generation with the use of the @higherkind annotation.
  • Understand the Arrow Kind abstractions and how the generated code uses it.
  • Create a bifunctor typeclass implementation for Result<E,A> using the @extension annotation.
  • Use the same process for the implementation of the applicative version of Result<E,A>.
  • Use the applicative in a classic validation use case.

Time to do some coding magic! :]

Note: This tutorial series was inspired by this wonderful course by Bartosz Milewski: Category Theory for Programmers.

Getting Started

Download the materials for this tutorial using the Download Materials button at the top or bottom of this page. Open the project using IntelliJ 2019.x or greater. Here’s the structure of the project:

ArrowFunctionalFetcher Initial structure

ArrowFunctionalFetcher Initial structure

It’s important to note that:

  1. You’re going to write most of main() in the external src folder. This is where FunctionalFetcher.kt is.
  2. The arrow module contains the data-types and typeclasses submodules. These depend on the Arrow library. You’ll need two modules because the code generation must be done before the one for typeclasses — which usually depends on the previous one.

Start by opening FunctionalFetcher.kt and locating the following code:

// 1
object FunctionalFetcher {
  fun fetch(url: URL): String {
    try {
      // 2
      with(url.openConnection() as HttpURLConnection) {
        requestMethod = "GET"
        // 3
        val reader = inputStream.bufferedReader()
        return reader.lines().asSequence().fold(StringBuilder()) { builder, line ->
          builder.append(line)
        }.toString()
      }
    } catch (ioe: IOException) {
      // 4
      throw FetcherException(ioe.localizedMessage)
    }
  }
}

The code above:

  1. Defines FunctionalFetcher which uses fetch() to get content from the network given a URL parameter.
  2. Opens HttpURLConnection with HTTP’s GET.
  3. Reads and accumulates all the lines into a String using StringBuilder.
  4. Throws FetcherException, if any errors occur. The exception is declared in the FetcherException.kt file in the arrow/data-types module.

You can test the previous code running the following main method found in the same FunctionalFetcher.kt file.

fun main() {
  // 1
  val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
  // 2
  val error_url = URL("https://error_url.txt")
  // 3
  println(FunctionalFetcher.fetch(ok_url))
}

This code is very simple. It:

  1. Defines the ok_url variable you can use to test FunctionalFetcher with a successful result.
  2. Uses error_url to test for errors.
  3. Invokes fetch() with one of the previous parameters.

If you run main() using ok_url, you’ll get output like this:

[  {    "userId": 1,    "id": 1,    "title": "delectus aut autem",    "completed": false  },   
  - - -
{    "userId": 10,    "id": 200,    "title": "ipsam aperiam voluptates qui",    "completed": false  }]

If you use error_url, you’ll get this output instead.

Exception in thread "main" com.raywenderlich.fp.FetcherException: error_url.txt

In the previous tutorial, Functional Programming with Kotlin and Arrow: More on Typeclasses, you learned how to use the Result<E, A> data type that you implemented from scratch. You can now do the same with the Arrow framework and see how this framework can make the process simpler.

Implementing the Result<E, A> Datatype With Arrow

First, create a new file named Result.kt in the arrow/data-type module and add the following code:

// 1
sealed class Result<out E, out A>
// 2
class Success<out A>(val a: A) : Result<Nothing, A>()
// 3
class Error<out E>(val e: E) : Result<E, Nothing>()

You’ll notice that it’s the same code you wrote in the previous tutorial. It:

  1. Defines Result<E,T> using a sealed class.
  2. Creates Success<T>, if successful. This encapsulates a result of type T.
  3. Creates Error<E>, if it fails. This encapsulates the exception of type E.

Seeing How Arrow Can Help?

But how can Arrow help? Arrow helps in the implementation of the typeclasses. But in order to do this, you need to make your Result<E,T> an implementation of the Kind interface. There are different … kinds (:]) of the Kind interface depending on the number of type parameters. For this Result<E,T>, you just need the following:

@documented
interface Kind<out F, out A>
typealias Kind2<F, A, B> = Kind<Kind<F, A>, B>

If you write your data type as an implementation of the Kind interface, Arrow can generate all the code you need. To do so, replace the previous code with the following:

// 1
@higherkind
// 2
sealed class Result<out E, out A> : ResultOf<E, A> {
  // 3
  companion object
}
class Success<out A>(val a: A) : Result<Nothing, A>()
class Error<out E>(val e: E) : Result<E, Nothing>()

There are some important things to note:

  1. You use the @higherkind annotation to enable the Arrow code generation for data types.
  2. The Result<E,T> sealed class now implements the ResultOf<E, A> interface which is not available yet. You’ll learn about this very soon.
  3. In order to simplify the code generation, Arrow needs an empty companion object which is used as an anchor point.

The code should not compile yet

The code should not compile yet

Generating Arrow Code

The previous code doesn’t compile because Arrow didn’t generate the code yet. You can do so by building the arrow/data-types module from your terminal using the following command:

./gradlew :arrow:data-types:build 

You can also run the same task using the equivalent option in the Gradle tab in IntelliJ:

Build the data-types module

Build the data-types module

Now the warnings in IntelliJ should disappear and the code should compile successfully.

The code now compile successfully

The code now compile successfully

But what is the ResultOf<E, A> interface?

Looking at the Generated Code

Building the arrow/data-types enables the Arrow code generation. With @higherkind annotation, you generate the code found in the arrow/data-types/build/generated/source/kaptKotlin folder:

Arrow generated code for data types

Arrow generated code for data types

In your case, the generated code is the following:

// 1
class ForResult private constructor() { companion object }
// 2
typealias ResultOf<E, A> = arrow.Kind2<ForResult, E, A>
// 3
typealias ResultPartialOf<E> = arrow.Kind<ForResult, E>

// 4
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
inline fun <E, A> ResultOf<E, A>.fix(): Result<E, A> =
  this as Result<E, A>

You’ve already seen similar code in the Functional Programming with Kotlin and Arrow Part 2: Categories and Functors tutorial. But in this case, you have:

  1. The definition of the ForResult type which might remind you of the Nothing type because it cannot have instances.
  2. The ForResult type as the first type parameter for Kind2.
  3. ResultPartialOf<E> as a convenience typealias you’ll use later for the implementation of the Bifunctor, Applicative and Monad typeclasses.
  4. The fix() method which will be very useful when you need to cast the ResultOf<E, A> type to the Result<E, A> you created earlier.

But what can you do now with the new ResultOf<E, A> implementation?