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

In the previous tutorial Functional Programming with Kotlin and Arrow Part 2: Categories and Functors, you had the opportunity to learn about functors, their meaning in the context of Category theory and how to use them in your code. You also implemented the Maybe functor using both plain Kotlin and the Arrow framework.

In this tutorial, you’ll use a specific and common-use case to go further and develop a better understanding of data types and typeclasses, from functor to monad. You’ll discover applicatives and semigroups, too.

Working through this tutorial, you’ll:

  • Learn what a data type is by using the Result<E,A> data type in a practical example.
  • Create a bifunctor typeclass implementation for Result<E,A>.
  • Discover what applicatives are and how to use one to solve a classical problem of creation and validation of entities.
  • Explore how a semigroup can be useful in cases of error management.
  • Understand what a monad is and why you need this kind of data type in your program.

Time to do some coding magic! :]

Note: This tutorial series has been inspired by the wonderful Bartosz Milewski’s Category Theory for Programmers course.

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. You can check out its structure in the following image:

FunctionalFetcher Project Structure in Functional Programming

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
class FetcherException(override val message: String) :
  IOException(message)

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

This code:

  1. Declares FetcherException as a custom exception.
  2. Defines FunctionalFetcher. It contains fetch() for, well, fetching some content from the network given a URL parameter.
  3. Opens a HttpURLConnection with the HTTP GET.
  4. Reads and accumulates all the lines into a String using a StringBuilder.
  5. Throws a FetcherException that encapsulates the error, if any.

You can run the previous code using main():

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

If you use ok_url, everything should be fine. The JSON content should be displayed as follows:

[ 
   { 
      "userId":1,
      "id":1,
      "title":"delectus aut autem",
      "completed":false
   },
   - - -
]

If you use the error_url, you’ll get an exception like this:

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

True, that’s a rather simple example. But you’re going to improve upon it using some interesting functional programming concepts.

Finding a Functional Fetcher

The fetch() you defined in the previous example has several problems. First, it’s not a pure function; it has some side effects. That’s because it can throw an exception, which isn’t one of the possible values for its return type Int.

Note: This is not completely true in Kotlin. The throw FetcherException(ioe.localizedMessage) expression is of type Nothing, a subtype of any type and therefore a subtype of Int, too. Even so, the side-effect problem remains.

In the first tutorial of this series, Functional Programming with Kotlin and Arrow: Getting Started, you learned how to deal with side effects. One potential solution is to move it as part of the return type for the function.

In this case, you could replace the previous code in FunctionalFetcher.kt with the following:

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

With this code, you have:

  1. Changed the signature of fetch() to return a Pair<FetcherException?, String?>. This makes it compatible with all the possible results in case of error or success. It’s important to note the nullability for the two different types of the Pair.
  2. In case of success, returned the pair instance with null as a value for the first property and the JSON text for the second.
  3. In case of error, set the exception as a value of the first property and null as a value for the second.

The function is now pure, but it isn’t type-safe. That’s because of Pair<FetcherException?, String?>. It can represent values where the first and second property is either present or missing. fetch() can succeed or fail, but not both.

Introducing the Result<E,T> Data Type

A potential solution to the previous type-safety problem is Result<E,T>. Remember that a data type allows you to add a specific context to some data. For instance, Maybe<T> allows you to represent the context of the presence or the absence for a value of type T. This doesn’t depend on the type T at all. It’s another dimension.

The same holds for Result<E,T>. Its context is the chance to contain a value of type E or a value of type T but not both. Again, this doesn’t depend on either E or T.

Note: Result<E,T> is very similar to Either<A,B>. Its context offers the chance to have only a value of type A or type B, but not both. This is more generally compared to Result<E,T>, where E represents a failure, while T represents a value that results from a successful operation.

Create a Result.kt file into the data-type submodule. Then copy 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>()

This code:

  1. Defines Result<E,T> using a sealed class.
  2. In case of success, creates Success<T> as the implementation that encapsulates a result of type T.
  3. In case of failure, creates Error<E> as the implementation that encapsulates the exception of type E.
Note: It’s interesting to point out how the type for the missing value is Nothing, a subtype of any other Kotlin type.

You can now use the Result<E,T> in a new version of your FunctionalFetcher. Replace the previous code in FunctionalFetcher.kt with the following:

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

The code is similar to before. But this time you:

  1. Defined fetch() with Result<FetcherException, String> as a return type.
  2. Returned a Success<String> that encapsulates the result in case of success.
  3. Returned a Error<FetcherException> that encapsulates the exception in case of error.

Besides type safety and purity, what advantages will you receive by doing this? To better understand, you’ll need to implement some useful typeclasses, starting from the functor.