Functional Programming with Kotlin and Arrow – Generate Typeclasses With Arrow


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! :]

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?

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

From this, Arrow will generate a lot of very useful code that’ll help you in the implementation of the FunctionalFetcher.kt feature.

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.

Implementing a Result Applicative With Arrow

In the previous sections, you spent some time understanding how the Arrow code generation process works. You defined your data type, annotated with @higherkind and generated the code to make it an implementation of the Kind interface. Then you implemented the Arrow interface related to the specific typeclass using the @extension annotation. After this, you used the generated code in your app.

You can now follow the same process to make the Result<E, T> data type an applicative.

In the arrow/typeclasses module, create a new file named ResultApplicative.kt and add the following content:


// 1
@extension
// 2
interface ResultApplicative<E> : Applicative<ResultPartialOf<E>> {
  // 3
  override fun <A> just(a: A): Result<E, A> = Success(a)
  // 4
  override fun <A, B> Kind<ResultPartialOf<E>, A>.ap(ff: Kind<ResultPartialOf<E>, (A) -> B>): Kind<ResultPartialOf<E>, B> {
    // 5
    if (ff.fix() is Error<E>) {
      return ff.fix() as Error<E>
    }
    // 6
    if (fix() is Error<E>) {
      return fix() as Error<E>
    }
    // 7
    val myRes = fix() as Success<A>
    val ffRes = ff.fix() as Success<(A) -> B>
    val ret = ffRes.a.invoke(myRes.a)
    return Success(ret)
  }
}

This is the code for the ResultApplicative<E> where you:

  1. Use the @extension annotation because you’re providing a typeclass implementation for a data type of yours.
  2. Create an interface which extends the Applicative<ResultPartialOf<E>> interface. Arrow generated the ResultPartialOf<E> along with the Result<E, T> data type and it represents the type with just one of the type parameters.
  3. Implement just() which is the first of the two functions you need for an Applicative, as you learned in the previous tutorial.
  4. Provide an implementation for ap.
  5. Return ff if it is an Error.
  6. Do the same if Result<E, T> itself is an Error.
  7. Apply the function in Success, if successful. You pass this as a parameter to the successful value in the Result<E, T> itself.

To see how to use this code, you can repeat the same validation example you saw in Functional Programming with Kotlin and Arrow – More on Typeclasses.

Validating With Applicative

To test the generated code for the Applicative typeclass, create a new file named UserValidation.kt in the main module and add the following code:


// 1
data class User(val id: Int, val name: String, val email: String)
// 2
val userBuilder = { id: Int -> { name: String -> { email: String -> User(id, name, email) } } }
// 3
typealias UserBuilder = (Int) -> (String) -> (String) -> User

In this code, you:

  1. Define a User data class as an example of a model class for a user with id, name and email properties.
  2. Create a curried version of the function which creates an instance of User from the values of its properties.
  3. Define a typealias for the previous function type.

You can now add the following code:


fun main() {
  // 1
  val idAp = 1.just<FetcherException, Int>()
  val nameAp = "Max".just<FetcherException, String>()
  val emailAp = "max@maxcarli.it".just<FetcherException, String>()
  val userAp = userBuilder.just<FetcherException, UserBuilder>()
  // 2
  val missingNameAp = Error(FetcherException("Missing name!"))
  // 3
  val errorFunction = { error: FetcherException -> println("Exception $error") }
  val successFunction = { user: User -> println("User: $user") }

  // 4
  Result.applicative<FetcherException>()
    .run {
      emailAp.ap(nameAp.ap(idAp.ap(userAp)))
    }.bimap(errorFunction, successFunction)

  // 5
  Result.applicative<FetcherException>()
    .run {
      emailAp.ap(missingNameAp.ap(idAp.ap(userAp)))
    }.bimap(errorFunction, successFunction)
}

In this example, you try to create an instance of the User class if successful or if you encounter an error. Here you:

  1. Create the applicatives for id, name and email parameters with valid values. You do this using just(). You also define applicatives for userBuilder.
  2. Create an applicative if you have an invalid value for name.
  3. Define successFunction and errorFunction to handle success or errors.
  4. Use the applicative with all valid parameters. It’s interesting to note how an applicative is also a Functor and, more specifically, a Bifunctor.
  5. Do the same with invalid parameters, if any.

When you run main(), you’ll get the following output:


User: User(id=1, name=Max, email=max@maxcarli.it)
Exception com.raywenderlich.fp.FetcherException: Missing name!

If successful, you’ll get a valid User instance. If not, you’ll get a message regarding the missing name.

Where to Go From Here?

Congratulations! You used Arrow code generation to create a Result<E, T> data type with the Bifunctor and Applicative typeclasses. In this project, you also have the code for the Monad implementation which you can use as an optional exercise. You’ve come a long way in your study of Functional Programming but the journey’s not over yet! In the next tutorial, you’ll see what Algebraic Data Types are and how you can keep doing magic with them.

You can download a completed version of the project using the Download Materials button at the top or bottom of this tutorial.

If you have any comments or questions, feel free to join in the forum below.

Source link

Leave a Reply