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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Using the Generated Code

Now you have everything you need for a better implementation of FunctionalFetcher.

The FuntionalFetcherResult file

The FuntionalFetcherResult file

Create a file named FunctionalFetcherResult.kt in the same package of the FunctionalFetcher.kt file in the main module. Add the following code:

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

In this code, you:

  1. Replace the previous return type String with Result<FetcherException, String>.
  2. Return the result encapsulated in a Success<String>, if successful.
  3. Return the exception into an object of type Error<FetcherException>, if you encounter an error.

If you want to test the previous code, you need something different from the main() method in the previous implementation. You need a different behavior if the result is a Success than what you’d want if the result is an Error. Specifically, you need a Bifunctor.

Implementing a Bifunctor With Arrow

In the Functional Programming with Kotlin and Arrow – More on Typeclasses tutorial you learned what a Bifunctor is. It is a way to apply different functions to Kind2 depending on its actual type. For the Result<E,T> data type, it’s a way to apply a different function for Success<T> versus an Error<E>.
Implementing a Bifunctor with Arrow is relatively simple; you just need to create an implementation of the existing arrow.typeclasses.Bifunctor interface which requires the following operation:

fun <A, B, C, D> Kind2<F, A, B>.bimap(fl: (A) -> C, fr: (B) -> D): Kind2<F, C, D>

The fl is the function you’ll apply for an Error and fr for Success.

To implement this, create a new file named ResultBifunctor.kt in the arrow/typeclasses module and add the following code:

// 1
@extension
// 2
interface ResultBifunctor : Bifunctor<ForResult> {
  // 3
  override fun <A, B, C, D> Kind2<ForResult, A, B>.bimap(fl: (A) -> C, fr: (B) -> D): Kind2<ForResult, C, D> {
    val fixed = fix()
    return when (fixed) {
      // 4
      is Error<A> -> Error(fl(fixed.e))
      // 5
      is Success<B> -> Success(fr(fixed.a))
    }
  }

  companion object
}

In this code, you define an interface that:

  1. Uses the @extension to enable Arrow code generation for typeclasses.
  2. Extends the Bifunctor interface for the ForResult type which is the one you use in the Kind2 abstraction.
  3. Provides implementation for the bimap() function.
  4. Uses the fixed version of the Result object checking if it’s an Error or a Success. If the former, it applies fl and returns the result into another Error object.
  5. Applies fr, if you have a success and returns the result into a new Success object.

You can now run this command from the terminal:

./gradlew :arrow:typeclasses:build 

…or use the same option in the Gradle tab. Either method will trigger the Arrow code generation for typeclasses.

Build Typeclasses

Build Typeclasses

Examining the Generated Code for Bifunctor

After the execution of the previous command, you’ll see a new file in the build/generated/source/kaptKotlin/main folder of the typeclasses module:

Generated code for the Bifunctor typeclass

Generated code for the Bifunctor typeclass

If you have a look at the content of the ResultBifunctor.kt file, you’ll see the code that Arrow generated. You can find the bimap() function implementation with the following signature:

fun <A, B, C, D> Kind<Kind<ForResult, A>, B>.bimap(arg1: Function1<A, C>, arg2: Function1<B, D>):
    Result<C, D>

You can also find utility functions like:

fun <A, B, C> Kind<Kind<ForResult, A>, B>.mapLeft(arg1: Function1<A, C>): Result<C, B> 

…which allow you to apply a single function to the E part of Result<E, T>.

Note the functions with the following signatures:

fun <X> rightFunctor(): Functor<Kind<ForResult, X>>

fun <X> leftFunctor(): Functor<Conested<ForResult, X>>

These allow you to use the E and T parts as different Functors.

For a better understanding of this, go back to the FunctionalFetcherResult.kt file in the main module and add the following code after the FunctionalFetcherResult implementation:

fun main() {
  // 1
  val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
  val error_url = URL("https://error_url.txt")
  // 2
  val errorFunction = { error: FetcherException -> println("Exception $error") }
  // 3
  val successFunction = { json: String -> println("Json $json") }
  FunctionalFetcherResult
    .fetch(ok_url)
    .bimap(errorFunction,successFunction) // 4
}

In this code, you:

  1. Define the ok_url and error_url variable in order to test your code.
  2. Create errorFunction() to use for Error. This prints the encapsulated exception.
  3. Create successFunction, for success. This prints the result.
  4. Invoke bimap() with the previous functions as parameters.

Note that bimap() is the one Arrow generates for you for the Kind2 implementation of ForResult
You can now run the code and see the different behavior for an error versus success.

Using an Alternative Option

You can also use the generated code in a different, more complex way. Replace main() in the previous block of code with the following:

fun main() {
  // 1
  val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
  val error_url = URL("https://error_url.txt")
  // 2
  val result = FunctionalFetcherResult.fetch(error_url)
  // 3
  when (result) {
    is Success<String> -> manageSuccess(result)
    is Error<FetcherException> -> manageError(result)
  }
}

Here you simply:

  1. Define ok_url and error_url to test your code.
  2. Invoke fetch() to return Result<E, T>.
  3. Invoke manageSuccess() or manageError() depending on the resulting type.

To compile, you’ll need to add the following as well:

fun manageSuccess(result: Success<String>) {
  // 1
  val successFunction = { json: String -> println("Json $json") }
  val rightFunctor = Result
    .bifunctor() // 2
    .rightFunctor<String>() // 3
    .lift(successFunction) // 4
  rightFunctor(result) // 5
}

fun manageError(result: Error<FetcherException>) {
  // 6
  val errorFunction = { error: FetcherException -> println("Exception $error") }
  val leftFunctor = Result
    .bifunctor()
    .leftFunctor<FetcherException>()
    .lift(errorFunction)
  // 7
  leftFunctor(result.conest())
}

This code defines the manageSuccess() and manageError() functions. Here you:

  1. Define the successFunction function which simply prints the content of the response.
  2. Invoke bifunctor() on Result, after code generation makes it available. This will get the reference to the Bifunctor implementation.
  3. Invoke rightFunctor<String>() to get the reference to the Functor for the right part of Result, which is String.
    Note: A Functor is a high order function which maps a function from a type (A) -> B to a function of type F(A) -> F(B). In your case, F is the right part of the Result<E, T>.
  4. Pass the reference to successFunction() as a parameter of lift(). If successful, you’ll get another function you can apply to the result.
  5. Apply rightFunctor() to the result.
  6. Do the same in manageError() but for the left part of the Result<E, T>.
  7. Apply leftFunctor to the conest version of the result. Note: You can get more information regarding this on the Arrow website. In general, a Conest> is a way to represent a function from a type A to a type Kind<F,A,C>.

Run the new main() implementation and verify that the behavior is the same for success or an error. You’ll find that the previous approach is better. But this last approach demonstrates a possible use of the Arrow generated code for Bifunctor.