How to use TaskEither in fpdart

Sandro Maglione

Sandro Maglione

Functional programming

Do you know what TaskEither is in functional programming (specifically in fpdart)?

TaskEither is the most common type used in a functional codebase

Yet, this type is the source of a lot of confusion.

In this post, I will make you understand TaskEither from a real-world example. Hopefully, after reading this you will start to see (and use) TaskEither over and over again.

Let's get into it!


Real example of TaskEither in action

The example for today's post is taken from a Github issue on fpdart's repository.

You have access to a StudentRepo class that gives you two methods:

getAllStudents: Used to fetch a list of Student

getAllCourses: Used to fetch a list of Course given a list of Student

Your goal is to fetch the list of students, then use it to fetch the list of courses. Easy enough.

student_repo.dart
import 'data.dart';
 
class StudentRepo {
  static Future<List<Student>> getAllStudents() async => [
        Student("Juan"),
        Student("Maria"),
      ];
 
  static Future<List<Course>> getAllCourses(List<Student> studentList) async =>
      [
        Course("Math"),
        Course("Physics"),
      ];
}

Below I report the data.dart file containing the definitions of Student and Course:

data.dart
class Student {
  final String name;
  Student(this.name);
}
 
class Course {
  final String name;
  Course(this.name);
}

Why you should not use Future

As you can see, the methods inside StudentRepo both return a Future.

We are going to assume that you don't have access to StudentRepo, so you cannot change its API.

What's wrong with Future? The problem with Future is that you are not in control of its execution. When you write getAllStudents() the Future is going to request the data immediately.

Furthermore, returning a Future makes your function impure. Since you are accessing an external API, every time you call the function, even with the same input, you are going to get a different output.

In functional programming we want to keep all our functions pure, and execute external requests only once in main.

This means that we need more control over our request. That is where TaskEither comes into play!

What is TaskEither

TaskEither is an abstraction over Future and Either. It is used to avoid executing your request immediately (like Future does). Instead, it gives you full control over its execution.

This below is the definition of TaskEither in fpdart:

task_either.dart
/// `TaskEither` in `fpdart`
final Future<Either<L, R>> Function() _run;

Let go over it step by step.

TaskEither wraps Future

As you can see from its definition, TaskEither is a Function that returns a Future.

The key here is having Function as a wrapper. This means that when you create a TaskEither, it will not execute anything until you explicitly call it.

This feature of TaskEither allows you to manipulate the data before executing any request.

TaskEither returns an Either

TaskEither is specifically designed to execute request that may fail (you should use Task if the request cannot fail).

In functional programming, we handle failures using Either. Therefore, TaskEither, when executed, will return Left when the request fails, or Right when the request is successful.

This allows you to explicitly handle the error (forget about try/catch and throw!).

Simple analogy

TaskEither (an functional programming in general) is like writing a plan of action on paper.

You define all the step that you will take, things like:

  • Which requests to make
  • What to do in each step in case of error
  • What to return at the end

All these instructions are not executed yet. It is just an outline of what your application is going to do.

When you decide that all the paths are defined and you are happy with your plan, you run it all together (inside main) and wait to see what you get back.

Imperative programming instead works iteratively.

You run the first function, wait for the result, then call the second, wait for the result, etc. In all this steps, you assume that the request never fails. You then wrap all your app in a try/catch statement and handle any possible error inside catch.

Convert a Future to TaskEither

Back to our example. The first step is to convert a function returning Future to a function that returns TaskEither.

In order to achieve this, we simply need to wrap the future in TaskEither.tryCatch():

What's tryCatch?

tryCatch is a constructor provided by TaskEither. It allows to wrap a function returning Future and catch any possible error.

When you then execute TaskEither you will get back a Either containing the result of your successful request, or a custom error defined by you.

In our example, we define a interface (abstract class in dart) for all possible errors:

errors.dart
abstract class ApiFailure {}
 
class StudentFailure implements ApiFailure {}
 
class CourseFailure implements ApiFailure {}

We then wrap getAllStudents and getAllCourses in TaskEither as follows:

errors.dart
TaskEither<ApiFailure, List<Student>> getStudents = TaskEither.tryCatch(
  () => StudentRepo.getAllStudents(),
  (_, __) => StudentFailure(),
);
 
TaskEither<ApiFailure, List<Course>> getCoursesOfStudents(
  List<Student> studentList,
) =>
    TaskEither.tryCatch(
      () => StudentRepo.getAllCourses(studentList),
      (_, __) => CourseFailure(),
    );

Why do we need the ApiFailure interface

If we want to compose each request one after the other, we need to have a shared error between all the requests.

That is because we need some way to map every possible error to a message to the user or similar. If all the functions have different error types, it becomes impossible to compose all the requests without mapping between different error types.

The solution is simple with abstract class in dart!

When we perform the request, if we detect any kind of error we can map it to a message by using is:

errors.dart
String logFailure(ApiFailure apiFailure) {
  if (apiFailure is StudentFailure) {
    return 'Error while fetching list of students';
  } else if (apiFailure is CourseFailure) {
    return 'Error while fetching list of courses';
  } else {
    throw UnimplementedError();
  }
}

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.

Function composition with TaskEither

Now our API is finally ready!

That is the interesting part. We are going to use the power of composition in functional programming to make our code simple and robust!

First of all, we are going to call getStudents to instantiate a TaskEither to request the list of students.

Now that we have our TaskEither, how do we use the list of students to fetch the list of courses if we cannot execute it?

Composition! TaskEither provides various methods to manipulate the final result without executing any request.

This feature allows our function to remain pure. That is because we are not making any external request. We are simply building the plan by defining all the steps to execute.

Chain TaskEither using flatMap

In this case, we want to access the list of students and use it to fetch the list of courses.

What we are doing is changing the final return type from List<Student> to List<Course>.

We use the flatMap method of fpdart. flatMap gives you access to the value stored inside TaskEither. You can execute any code using this value and return another TaskEither.

That is exactly our case! (Any generally the most common case in general 💁🏼‍♂️):

main.dart
/// How to call `getCoursesOfStudents` only if students is `Right`?
///
/// Type: `TaskEither<ApiFailure, List<Course>>`
final taskEitherRequest = getStudents.flatMap(getCoursesOfStudents);

Mapping the error using mapLeft

The last requirement is to map (possible) resulting error to a user-friendly message.

We use the mapLeft method of TaskEither. This method allows to change the type of error inside the TaskEither.

We pass the logFailure function that we defined above to mapLeft:

main.dart
/// In case of error map `ApiFailure` to `String` using `logFailure`
///
/// Type: `TaskEither<String, List<Course>>`
final taskRequest = taskEitherRequest.mapLeft(logFailure);

Running our plan

We are ready to run our TaskEither!

We defined all the steps to execute (our plan). Now we just need to call run to actually execute the external request and get back our result:

main.dart
/// Run everything at the end!
///
/// Type: `Either<String, List<Course>>`
final result = await taskRequest.run();

result is of type Either. This means that it will contain either a Right with the list of courses, or a Left with the error message.

We came back safely to the domain of our application. We can now decide how to handle the Either as we want.


The full code is available in fpdart's repository.

I also report the complete example here below 👇

data.dart
class Student {
  final String name;
  Student(this.name);
}
 
class Course {
  final String name;
  Course(this.name);
}
failure.dart
abstract class ApiFailure {}
 
class StudentFailure implements ApiFailure {}
 
class CourseFailure implements ApiFailure {}
student_repo.dart
import 'data.dart';
 
class StudentRepo {
  static Future<List<Student>> getAllStudents() async => [
        Student("Juan"),
        Student("Maria"),
      ];
 
  static Future<List<Course>> getAllCourses(List<Student> studentList) async =>
      [
        Course("Math"),
        Course("Physics"),
      ];
}
main.dart
import 'package:fpdart/fpdart.dart';
 
import 'data.dart';
import 'failure.dart';
import 'student_repo.dart';
 
TaskEither<ApiFailure, List<Student>> getStudents = TaskEither.tryCatch(
  () => StudentRepo.getAllStudents(),
  (_, __) => StudentFailure(),
);
 
TaskEither<ApiFailure, List<Course>> getCoursesOfStudents(
  List<Student> studentList,
) =>
    TaskEither.tryCatch(
      () => StudentRepo.getAllCourses(studentList),
      (_, __) => CourseFailure(),
    );
 
String logFailure(ApiFailure apiFailure) {
  if (apiFailure is StudentFailure) {
    return 'Error while fetching list of students';
  } else if (apiFailure is CourseFailure) {
    return 'Error while fetching list of courses';
  } else {
    throw UnimplementedError();
  }
}
 
void main() async {
  /// How to call `getCoursesOfStudents` only if students is `Right`?
  ///
  /// Type: `TaskEither<ApiFailure, List<Course>>`
  final taskEitherRequest = getStudents.flatMap(getCoursesOfStudents);
 
  /// In case of error map `ApiFailure` to `String` using `logFailure`
  ///
  /// Type: `TaskEither<String, List<Course>>`
  final taskRequest = taskEitherRequest.mapLeft(logFailure);
 
  /// Run everything at the end!
  ///
  /// Type: `Either<String, List<Course>>`
  final result = await taskRequest.run();
}

To be honest, I feel there is a lot more to uncover.

Don't worry if you feel confused. I am planning to write more posts like this which will go more into the details of everything that we discussed here (and more).

If you have any question, feel free to reach out to my Twitter.

If you are interested in this topic, consider subscribing to my newsletter here below 👇

Thanks for reading 🙏🏼

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.