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

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.