Functional Programming with Kotlin and Arrow – More on Typeclasses

Continuing the Functional Programming with Kotlin and Arrow Part 2: Categories and Functors tutorial, you’ll now go even further, using a specific and common use case, with a better understanding of data types and typeclasses, from Functor to Monad, passing through Applicatives and Semigroups. By Massimo Carli.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Implementing Result<E,T> as Functor

You can think of a typeclass as a definition that abstracts a common behavior between different data types. A functor is probably one of the most common. It provides a function map() that, for Result<E,T>, can have two different flavors if applied to the success value of the error.

To define this, create a ResultFunctor.kt into the typeclasses sub-module. Then copy the following code:

fun <E1, E2, T> Result<E1, T>.mapLeft(fn: (E1) -> E2): Result<E2, T> = when (this) {
  is Success<T> -> this
  is Error<E1> -> Error(fn(this.e))
}

The function you pass as a parameter has type (E1) -> E2, and it has effect only in case Result<E,T> is an Error<E>. In case it’s a Success<T>, mapLeft() returns the same object.

You can follow the same approach for Success<T>. Copy this code in the same file:

fun <E, T, R> Result<E, T>.mapRight(fn: (T) -> R): Result<E, R> = when (this) {
  is Success<T> -> Success(fn(this.a))
  is Error<E> -> this
}

In this case, the function you pass as a parameter has type (T) -> R. mapRight() applies the function, but only if the current Result<E,T> is a Success<T>. If it’s an Error<E>, it returns the same object.

Because there are two different versions of the map() function, this typeclass is also called a bifunctor. You can provide another version of this by adding the following code to the same file:

fun <E1, E2, T, R> Result<E1, T>.bimap(fe: (E1) -> E2, fs: (T) -> R): Result<E2, R> = when (this) {
  is Success<T> -> Success(fs(this.a))
  is Error<E1> -> Error(fe(this.e))
}

As you can see, the implementation of bimap() has both the functions for the success and error cases as parameters.

Practicing with the Bifunctor

You can now go back to FunctionalFetcher.kt and replace the existing main() with the following:

fun main() {
  val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
  val error_url = URL("https://error_url.txt")
  // 1
  val printErrorFun = { ex: FetcherException -> println("Error with message ${ex.message}") }
  // 2
  val printString = { str: String -> print(str) }
  // 3
  FunctionalFetcher.fetch(error_url)
    .bimap(printErrorFun, printString)
}

This is similar to before. But this time you:

  1. Defined printErrorFun(), which prints out the message of a FetcherException.
  2. Created printString(), which prints out the content fetched from the network as a simple String.
  3. Used the two functions as a parameter for the bimap() you defined earlier.

Now, you can easily run the code and see the output for success or failure.

In case of success, you can print the text you receive from the network. It will look something like this:

[  {    "userId": 1,    "id": 1,    "title": "delectus aut autem",    "completed": false  },  {    "userId": 1,    "id": 2,    "title": "quis ut nam facilis et officia qui",    
- - -
{     false  }]

In case of error, however, you’ll see this error message:

Error with message error_url.txt

Defining the Applicative Functor

The applicative is another important typeclass. It adds two new functions to the functor.

Start by creating a new ResultApplicative.kt in the typeclass submodule. Then copy the following code:

fun <T> justResult(value: T) = Success(value)

This code defines justResult(). This is similar to the type constructor you saw in a previous tutorial. It’s a function that accepts a value of type T and creates an instance of a specific data type — Result<E,T> in this case.

You can typically find this function with the name just() or pure(). You use the justResult() to avoid the usage of companion objects or extension functions and leverage the type inference Kotlin provides.

After this, you can copy the following code in the same file:

// 1
fun <E, T, R> Result<E, T>.ap(fn: Result<E, (T) -> R>): Result<E, R> = when (fn) {
  // 2
  is Success<(T) -> R> -> mapRight(fn.a)
  is Error<E> -> when (this) {
    // 3
    is Success<T> -> Error(fn.e)
    // 4
    is Error<E> -> Error(this.e)
  }
}

In this code, you define the ap() function. It:

  1. Is similar to a functor. However, an important difference is the type of function parameter, Result<E, (T) -> R>. The function you want to apply is encapsulated into the same data type you’re creating as an applicative.
  2. Applies the function you’re passing as a parameter of type Result<E, (T) -> R>, in case it’s a Success<(T)->R>. Here, you invoke mapRight(), which already takes care of the case when the current object is an Error<E>.
  3. Returns Error<E> to encapsulate the error of the parameter in case it’s an Error<E>.
  4. Returns Error<E> to encapsulate the error into the current object in case it’s an Error<E>.

An example will help you to better understand how this works.

Setting the Applicative to Work

Let’s create an class called User. You can define it by copying the following definition into a new User.kt. Create the following in the main src folder:

data class User(val id: Int, val name: String, val email: String)

This data class has three mandatory properties you must provide to have a valid user. To do so, add a builder function to User.kit:

val userBuilder = { id: Int -> { name: String -> { email: String -> User(id, name, email) } } }

The type of this function is:

typealias UserBuilder = (Int) -> (String) -> (String) -> User
Note: This type may seem difficult to understand, but it’s just a curried version of the User constructor. You’ll learn about currying in a future tutorial.

You can test userBuilder() by copying and running the following code into the same file:

fun main() {
  // 1
  val idAp = justResult(1)
  val nameAp = justResult("Max")
  val emailAp = justResult("max@maxcarli.it")
  // 2
  val userAp = justResult(userBuilder)
  // 3
  emailAp.ap(nameAp.ap(idAp.ap(userAp))).mapRight {
    println(it)
  }
}

Here, you have:

  1. Encapsulated the value you want to use as parameters into a Success of the related type using the justResult() function.
  2. Done the same for the userBuilder() function.
  3. Invoked ap() on the Success. For the id to get another Success, you pass one for the name. Finally, you do the same for the one about the email.

The result is a Success<User>. You know this is a functor you can call mapRight() on. Because everything is in place, you’ll get the following output:

User(id=1, name=Max, email=max@maxcarli.it)

As a counter-example, you can change the code in main() with the following where the name is missing:

fun main() {
  val idAp = justResult(1)
  // 1
  val missingNameAp = Error(IllegalStateException("Missing Name!"))
  val emailAp = justResult("max@maxcarli.it")
  val userAp = justResult(userBuilder)
  // 2
  emailAp.ap(missingNameAp.ap(idAp.ap(userAp))).mapLeft {
    println(it)
  }
}

With this code, you’ve:

  1. Created an Error<IllegalStateException> as value for the name.
  2. Used mapLeft() to display the error message.

Build and run. You’ll see the following output:

java.lang.IllegalStateException: Missing name!

This means the User has not been created because the mandatory name is missing.

At this point, you probably noticed that the syntax is not easy to write and wondering if a different approach may be better. You’re absolutely correct! And functional programming can help you devise better syntax.