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.
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
:
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
:
/// `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:
abstract class ApiFailure {}
class StudentFailure implements ApiFailure {}
class CourseFailure implements ApiFailure {}
We then wrap getAllStudents
and getAllCourses
in TaskEither
as follows:
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
:
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 π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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 ππΌββοΈ):
/// 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
:
/// 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:
/// 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 π
class Student {
final String name;
Student(this.name);
}
class Course {
final String name;
Course(this.name);
}
abstract class ApiFailure {}
class StudentFailure implements ApiFailure {}
class CourseFailure implements ApiFailure {}
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"),
];
}
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 ππΌ