In this series we are going to learn how to convert the Open Meteo API example (from the bloc package) from imperative code to functional programming using fpdart.
We will see step-by-step how to refactor the code to functional programming, as well as the benefits that this brings.
Each post of the series will cover in details one specific aspect of the refactoring.
In this first post:
- Introduction to the Open Meteo API code example
- Refactoring of the return type of the function from
FuturetoTaskEither- What is the difference between
FutureandTask - How
TaskEitherworks (Taskasync +Eithererror handling) - How to encode errors using
Eitherandabstract class
- What is the difference between
Open Meteo API example
We are going to refactor the Open Meteo API client using fpdart and functional programming.
This example is part of the flutter_weather app example in the bloc package.
The implementation is relatively simple. Nonetheless, it contains some of the most usual usecases of a real production app, like:
- Making an http request (using the
httppackage) - Checking for a valid response
- Decoding the response from JSON
- Validating the response format
We are going to focus specifically on the locationSearch request, that you can see reported below 👇:
/// Finds a [Location] `/v1/search/?name=(query)`.
Future<Location> locationSearch(String query) async {
final locationRequest = Uri.https(
_baseUrlGeocoding,
'/v1/search',
{'name': query, 'count': '1'},
);
final locationResponse = await _httpClient.get(locationRequest);
if (locationResponse.statusCode != 200) {
throw LocationRequestFailure();
}
final locationJson = jsonDecode(locationResponse.body) as Map;
if (!locationJson.containsKey('results')) throw LocationNotFoundFailure();
final results = locationJson['results'] as List;
if (results.isEmpty) throw LocationNotFoundFailure();
return Location.fromJson(results.first as Map<String, dynamic>);
}The goal is to see step by step what you must consider to convert this implementation to functional programming using fpdart.
Furthermore, we are going to highlight the reason behind each refactoring, showing the potential benefits along the way.
Future is not your friend
Let's start from the very top: the return type, Future<Location>. Here is a general rule that you must follow when working with fpdart and functional programming:
Do not return
Futurefrom a function, useTask,TaskOption, orTaskEitherinstead
You can read my previous post about sync and async functions, and why Future is not functional-friendly.
The main reason is that Future makes the function impure: the result of calling the function will change at every request. Furthermore, Future is not composable and it makes error handling complex and verbose.
I strongly suggest you to read this post about Future & Task: asynchronous Functional Programming, which goes into more details on the difference between
FutureandTask
fpdart has 3 alternatives, each suited for different situations based on the requirements:
Task: Used to perform async requests that you are 100% sure that will never fail (100% ☝️)TaskOption: Used to perform async requests that may fail and/or return a missing valueTaskEither: Used to perform async requests that may fail for multiple reasons that we want to encode in the return type
What makes Task functional programming friendly?
You may be thinking: "Wait, but making an http request will always return a different result on every call of the function, how can Task make the function pure?".
Simple: Task doesn't make the request, yet. In fact, Task is implemented as follows 👇:
class Task<A> {
final Future<A> Function() _run;
/// Build a [Task] from a function returning a [Future].
const Task(this._run);
/// Run the task and return a [Future].
Future<A> run() => _run();Task is a thunk: a function that, when called, will return a Future.
Until you call the run() method, not request is executed.
Taskis simply a description of your request, it doesn't do anything untilrun()is called 💁🏼♂️
That's what makes the function pure and composable: you can now chain methods to extract and validate the response, without actually making any request 🪄.
Again, you can learn more about
Taskin Future & Task: asynchronous Functional Programming
Which Task should I choose?
Task, TaskOption, or TaskEither?
As a rule of thumb, TaskEither is generally what you are looking for.
TaskEither allows to encode the error directly in the response type. It takes 2 generic parameters (TaskEither<L, R>):
L(Left): represents the error type in case the request failsR(Right): represent the response type in case of a successful request
We are going to use TaskEither in this example. What types should we give it? 🤔
Error handling with the Either type
In functional programming, the response type of the function should give us all the information needed to handle all possible events inside the app.
TaskEither (which is a fusion of Task + Either) allows to do just that:
- Since it's a
Task, we know we are making an async request - Since it's an
Either, we know the request can fail
Furthermore, Either<L, R> will give us even more information:
- If the function fails, all the possible failures are described by the
L(Left) type - If the function succeeds, the response type is
R(Right)
Just by reading the return type, we know everything that we need to handle any possible response in the app (regardless of how the function is implemented internally)
How to encode any possible error using Either
The function can fail for multiple reasons, but we have only 1 error type allowed.
That's the perfect usecase for an abstract class.
We define an abstract class which represents our error:
/// Abstract class which represents a failure in the `locationSearch` request.
abstract class OpenMeteoApiFpdartLocationFailure {}Now all the possible errors in the response will implement OpenMeteoApiFpdartLocationFailure, for example:
/// [OpenMeteoApiFpdartLocationFailure] when location response
/// cannot be decoded from json.
class LocationInvalidJsonDecodeFpdartFailure
implements OpenMeteoApiFpdartLocationFailure {
const LocationInvalidJsonDecodeFpdartFailure(this.body);
final String body;
}Finally, we can define the response type of the locationSearch function:
/// Finds a [Location] `/v1/search/?name=(query)`.
TaskEither<OpenMeteoApiFpdartLocationFailure, Location> locationSearch(String query)Notice how the function is no more
async, since it does not return aFuture
To recap:
TaskEither: async request that can failOpenMeteoApiFpdartLocationFailure: class which encodes all the possible errorsLocation: result of a successful response
That is all for this first part.
In the second part of this series we are going to learn how to make http requests in functional programming, and how to validate the response.
You can subscribe to the newsletter here below to stay up to date with each new post 👇
