Home · Android & Kotlin Tutorials

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.

3/5 1 Rating

Version

  • Kotlin 1.3, Other, IntelliJ IDEA

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.

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.

Devising a Better Syntax For Applicatives

In the previous example, you had to deal with gaggles of parentheses and dots. Kotlin and functional programming can help you with that.

Add the following to ResultApplicative.kt in the typeclasses sub-module:

infix fun <E, A, B> Result<E, (A) -> B>.appl(a: Result<E, A>) = a.ap(this)

Here, you basically flip the receiver of the function with its argument. You can now replace the previous code for main() in User.kt with the following:

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

Here’s what’s happening in the code:

  1. Create the User and passing the values for its parameters in order.
  2. Using the same code but getting an Error<IllegalStateException> in case any of the parameters are missing.

Run the code. You’ll get the following output for the different use cases:

User(id=1, name=Max, email=max@maxcarli.it)
java.lang.IllegalStateException: Missing name!

Using an Applicative for Validation

The previous code is good, but you can do better. For instance, you can add the following in UserValidation.kt in the main module. Create a file called UserValidation.kt and copy the following code in:

// 1
class ValidationException(msg: String) : Exception(msg)

// 2
fun validateName(name: String): Result<ValidationException, String> =
  if (name.length > 4) Success(name) else Error(ValidationException("Invalid Name"))

// 3
fun validateEmail(email: String): Result<ValidationException, String> =
  if (email.contains("@")) Success(email) else Error(ValidationException("Invalid email"))

With this code you’ve:

  1. Created ValidationException, which encapsulates a possible validation error message.
  2. Defined validateName() to check if the name is longer than four characters.
  3. Created validateEmail() to validate the email and check if it contains the @ symbol.

Both functions return a Error<ValidationException> if the validation fails. You can now use them like in the following code.

Copy the following code into UserValidation.kt, then run the method.:

fun main() {
  // 1
  val idAp = justResult(1)
  val userAp = justResult(userBuilder)
  // 2
  val validatedUser = userAp appl idAp appl validateName("Massimo") appl validateEmail("max@maxcarli.it")
  // 3
  validatedUser.bimap({
    println("Error: $it")
  }, {
    println("Validated user: $it")
  })
}

With this code, you’ve:

  1. Initialized idAp and userAp as before.
  2. Used validateName() and validateEmail() to validate the input parameter.
  3. Printed the result using the bimap() you created earlier.

This code should now create the User, but only if the validation is successful or an incorrect validation displays the error message.

Learning More About Errors With Semigroups

In the previous code, you learned a possible usage for the applicative typeclass using some validator functions. You can still improve how to manage validation error, though.

Create a new file named UserSemigroup.kt in the main module. Then copy and run the following code:

fun main() {
  val idAp = justResult(1)
  val userAp = justResult(userBuilder)
  val validatedUser = userAp appl idAp appl validateName("Max") appl validateEmail("maxcarli.it")
  validatedUser.bimap({
    println("Error: $it")
  }, {
    println("Validated user: $it")
  })
}

You’ll get the following output:

Error: com.raywenderlich.fp.ValidationException: Invalid email

Although both the validators fail, you only get the last error message. You lost the information about the first validation error for the length of the name. In cases like this, you can use semigroup typeclasses. Basically, these define how to combine the information encapsulated in the context of two data types.

To understand how this works, create a new file named ValidationSemigroup.kt in the typeclasses sub-module. Then copy the following code:

// 1
interface Semigroup<T> {
  operator fun plus(rh: T): T
}

// 2
class SgValidationException(val messages: Array<String>) : Semigroup<SgValidationException> {
  // 3
  override operator fun plus(rh: SgValidationException) =
    SgValidationException(this.messages + rh.messages)
}

With this code you’ve:

  1. Created Semigroup<T>, an interface that abstracts objects with the plus() operation.
  2. Defined a new SgValidationException to encapsulate an array of error messages and implement Semigroup<SgValidationException>. This means you can add an instance of SgValidationException to another one.
  3. Overloaded the + operator in a way that combines two instances of SgValidationException to create a new one with error messages that unit both.

You can now replace the implementation of ap() into the ResultApplicative.kt file. Use the following:

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

Here you’ve:

  1. Added the constraint to the type E for being a Semigroup<E>.
  2. Combined the errors in case both the objects in play are Error<E>.

In the same file, you have to replace the current implementation for appl() with the following:

infix fun <E : Semigroup<E>, A, B> Result<E, (A) -> B>.appl(a: Result<E, A>) = a.ap(this)

This will add the same constraints to the type E and will break the main() into the User.kt file. You can now replace that file with the following code, which uses Error<SgValidationException> as error type:

fun main() {
  val idAp = justResult(1)
  val nameAp = justResult("Max")
  val missingNameAp = Error(SgValidationException(arrayOf("Missing name!"))) // HERE
  val emailAp = justResult("max@maxcarli.it")
  val userAp = justResult(userBuilder)
  // 1
  (userAp appl idAp appl nameAp appl emailAp).mapRight { println(it) }
  // 2
  (userAp appl idAp appl missingNameAp appl emailAp).mapLeft { println(it) }
}

Next, replace validateName() and validateEmail() in UserValidation.kt with the following code:

fun validateName(name: String): Result<SgValidationException, String> =
  if (name.length > 4) Success(name)
  else Error(SgValidationException(arrayOf("Invalid Name")))

fun validateEmail(email: String): Result<SgValidationException, String> =
  if (email.contains("@")) Success(email)
  else Error(SgValidationException(arrayOf("Invalid email")))

Here, you’ve replaced ValidationException with SgValidationException, which needs an array of error messages. You can now test how it works by replacing main() in UserValidation.kt with the following:

fun main() {
  val idAp = justResult(1)

  val userAp = justResult(userBuilder)

  val validatedUser = userAp appl idAp appl validateName("Max") appl validateEmail("maxcarli.it")
  validatedUser.bimap({
    it.messages.forEach {
      println("Error: $it")
    }
  }, {
    println("Validated user: $it")
  })
}

Because both the validations are not successful, you’ll get an output like this:

Error: Invalid email
Error: Invalid Name

As you can see, it contains both error messages. This is because SgValidationException is a Semigroup.

Presenting Monads

In the previous section, you implemented Result<E,A> as a functor and applicative that you can use for the FunctionalFetcher. The result is a JSON you store as a type String. In practice, you need to parse the JSON and return an object of a different type.

Create a file named FunctionalFetcherApp.kt into the main module. Then copy 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))
    }
  }
}

This should be nothing new. You’ve simply:

  1. Defined fetch() using the Result<FetcherException,String> as a return type.
  2. Returned a Success<String> if you successfully receive some data.
  3. Returned an Error<FetcherException> in case of an error.

In case of success, you’ll get a text containing the JSON. It will look like this:

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

It contains the JSON for an array of items that you can represent with the following data class, which you can copy in the same Kotlin file. It also represents a Task in an application for your TODOs.

@Serializable
data class Task(val userId: Int, val id: Int, val title: String, val completed: Boolean)
Note: This example uses the koltinx serialization plugin. But that plugin is beyond the scope for the current tutorial.

You now need to parse JSON into a List<Task>. You can do this with the following function copied into the same file:

fun parseTasks(jsonData: String): Result<JsonDecodingException, List<Task>> {
  val json = Json(JsonConfiguration.Stable)
  try {
    return Success(json.parse(Task.serializer().list, jsonData))
  } catch (ex: JsonDecodingException) {
    return Error(ex)
  }
}

This code tries to parse String in input. It returns a Success<List<Task>> in case of success or an Error<JsonDecodingException> in case of error.

Suppose you want to use this function to parse the result of your FunctionalFetcherResult object. You can then copy the following code into the same FunctionalFetcherApp.kt:

fun main() {
  val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
  val error_url = URL("https://error_url.txt")
  // 1
  FunctionalFetcherResult.fetch(ok_url).mapRight {
    // 2
    parseTasks(it)
  }.mapLeft {
    // 3
    println("Error $it")
  }.mapRight {
    // 4
    println("Output $it")
  }
}

In this code you’ve:

  1. Invoked fetch() with the ok_url.
  2. Used parseTasks() as a parameter to map().
  3. Printed the error message in case of an error.
  4. Printed the result in case of success.

Build and run. In case of success, you’ll get an output like the following:

Output com.raywenderlich.fp.Success@131276c2

This is because of the parseTasks() in mapRight. It returns a Result<JsonDecodingException, List<Task>>, which is the one you print in the end. In case of success, it would be nice to have List<Task> directly as an implicit parameter for the last mapRight. This is where the monad comes in.

Creating the Result<E, T> Monad

Crete a new ResultMonad.kt in the typeclass sub-module. Copy the following code:

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

Here you define a monad typeclass with flatMap(). This function:

  1. Has a parameter of type (T) -> Result<E, R>.
  2. Returns the result of the function you get as parameter on the current value. In this case, the current object is a Success<T>.
  3. Returns the same object in case of an Error<E>.

Now you can replace main() in FunctionalFetcherApp.kt with the following:

fun main() {
  val ok_url = URL("https://jsonplaceholder.typicode.com/todos")
  val error_url = URL("https://error_url.txt")
  FunctionalFetcherResult.fetch(ok_url)
    .flatMap(::parseTasks) // HERE
    .mapLeft {
      println("Error $it")
    }.mapRight {
      println("Output $it")
    }
}

You just replaced the first mapRight() invocation with the flatMap() you just define. Build and run.

If successful, you’ll now get an output like this:

Output [Task(userId=1, id=1, title=delectus aut autem, completed=false), Task(userId=1, id=2, title=quis ut nam facilis et officia qui, completed=false), 
- - -
Task(userId=10, id=200, title=ipsam aperiam voluptates qui, completed=false)]

Now, if successful, the last mapRight() receives the result of parseTasks(), a function that can fail. In that case, you’d still get the JsonDecodingException as the type of the implicit parameter for mapLeft().

Where to Go From Here?

Congratulations! You just wrote a bunch of code. You implemented a Result<E, T> data type, and you handled the superpower of a bifunctor, an applicative and a monad. You also had the chance to use the semigroup typeclass in case of error management.

Your journey through the world of functional programming doesn’t end here, though. In the next tutorial, you’ll learn how Arrow can help generate most of the code you just wrote from scratch. You’ll have the chance to work on this same project and see how Arrow can help with the implementation of the FunctionalFetcher use case.

You can download a complete 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.

Average Rating

3/5

Add a rating for this content

1 rating

More like this

Contributors

Comments